前端八股文面试题

1.延迟加载JS有哪些方式?

延迟加载:async、defer
	例如:<script async src="script.js"></script>
defer:等html全部解析完成,才会执行js代码,顺次执行js脚本
async:异步解析js脚本,不会阻塞主线程,先解析完的脚本先执行

2.JS数据类型有哪些?

基本类型:string,number,boolean,undefined,null,symbol,bigint
引用类型:object

3.null 和 underfined 的区别?

本身都表示“没有”,但null表示引用类型的对象为空,undefined则表示变量未定义。

在相等判断时候,nullundefined是相等的

但nullundefined在很多方面有区别。

含义不同

null表示对象空指针,undefined表示变量未定义。

类型不同

typeof null // 'object'
typeof undefined // 'undefined'


Number(null) // 0
Number(undefined) // NaN

应用场景不同

null:

作为对象原型链的终点 

undefined:

定义了变量,没有初始化,默认是undefined

函数不return,或者return后面没有值,则函数默认返回undefined

函数参数如果不传,默认是undefined

4.== 和 === 有什么不同?

===如果类型不同返回false,如果类型相同则比较值是否相同,注意引用类型对象之和自身相等。

== 转换规则:  如果是类型相同,直接进行===比较,如果类型不同,要进行转换再比较。

5.如何理解 JS 的异步

JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
渲染主线程承担着诸多的工作,渲染页面,执行 JS 都在其中运行。
如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。
这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器,网络请求,事件监听等,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器用不阻塞,从而最大限度的保证了单线程的流畅运行。

6.阐述一下 JS 的事件循环

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据 W3C 官网的解释,每个任务有不同的类型,同类型的任务必须在同一队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。

消息队列是有优先级的
● 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。
在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
● 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行。
优先级执行顺序:微队列>延时队列>交互队列

7.作用域

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合

一般将作用域分成:全局作用域,函数作用域,块级作用域

全局作用域:
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问
// 全局变量
var greeting = 'Hello World!'
function greet() {
  console.log(greeting)
}
// 打印 'Hello World!'
greet()

函数作用域:
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
function greet() {
  var greeting = 'Hello World!'
  console.log(greeting)
}
// 打印 'Hello World!'
greet()
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting)

块级作用域:
ES6 引入了letconst关键字,var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变量
{
  // 块级作用域中的变量
  let greeting = 'Hello World!'
  var lang = 'English'
  console.log(greeting) // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang)
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting)

8.作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

下面代码演示下:

var sex = '男'
function person() {
  var name = '张三'
  function student() {
    var age = 18
    console.log(name) // 张三
    console.log(sex) // 男
  }
  student()
  console.log(age) // Uncaught ReferenceError: age is not defined
}
person()

上述代码主要主要做了以下工作:

 - student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出“张三”
 - student内部输出 sex 时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出“男”
 - 在person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到则报错

9.new一个对象发生了什么(构造函数的执行过程?)

1. 创建了一个空的js对象(即{}2. 将空对象的原型prototype指向构造函数的原型

3. 将空对象作为构造函数的上下文(改变this指向)

4. 判断构造函数的返回值,以决定最终返回的结果。

  a. 如果返回值是基础数据类型,则忽略返回值;

  b. 如果返回值是引用数据类型,则使用return 的返回,也就是new操作符无效;

10.原型与原型链

构造函数有个prototype对象(原型),该对象有个“constructor”属性,指向构造函数。

每个对象都有一个“proto”属性,指向它的构造函数的“prototype”属性。

构造函数的prototype对象,也有一个“proto”对象,它指向Object的prototype对象。

当我们访问对象中的属性时候,会先访问该对象中的本身的属性(私有属性),如果访问不到,会查找对象的“proto”指向的构造函数的prototype对象,如果其中有要访问的属性,就使用该值,否则继续访问prototype的“proto”,在其中查找要访问属性。这样一直上溯到Object对象。这个就是“原型链”。

11.数组的常用方法有哪些?


 - 遍历方法
map : 映射数组,得到一个映射之后的新数组
filter:筛选数组
forEach:遍历数组
some:判断数组是否有元素满足条件(相当于逻辑或:一真则真,全假为假)
every:判断数组是否所有满足都满足条件(相当于逻辑与:一假则假,全真为真)
findIndex:查找元素下标,一般用于元素是引用类型
reduce:给数组每一个元素执行一次回调,一般用于数组元素求和(也可以求最大值、最小值)

 - 增删改查方法
push() : 末尾新增元素,返回值是新数组长度
unshift():开头新增元素,返回值是新数组长度
pop() :末尾删除元素,返回值是删除的那个末尾元素
shift(): 开头删除元素,返回值是开头的那个末尾元素
splice() : 删除指定下标元素,第三个参数是一个剩余参数,可以在删除的元素后面插入元素

 - 其他方法
reverse:翻转数组,会修改数组自身
sort: 数组排序,会修改数组自身
json: 拼接数组元素,返回值是拼接之后的字符串
slice: 根据下标范围查询数组元素,返回值是查询后的新数组
indexOf: 查询元素下标,一般用于元素是值类型

12.字符串的常用方法有哪些?

indexOf: 查询某个字符下标,一般用于判断字符串中是否包含某些字符
split: 切割字符串,返回值是切割后的新数组
substring: 截取字符串,返回值是截取后的字符串
replace: 替换字符串,返回值是替换后的新字符串
toLowerCase : 转小写
toUpperCase : 转大写

13.箭头函数和普通函数有什么区别

1. 箭头函数在一些情况下书写更简洁(如只有一个参数、函数体直接返回值时候)。

2. 箭头函数没有自己的this,箭头函数内的this变量指向外层非箭头函数的函数的this,或者将该箭头函数作为属性的对象。箭头函数也不支持call()/apply()函数特性。

3. 箭头函数内部不可以使用arguments对象。

4. 箭头函数不可以当做构造函数。

为什么不能用作构造函数:
构造函数是通过new关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this的过程,而箭头函数没有自己的this。创建对象过程,new 首先会创建一个空对象,并将这个空对象的__proto__指向构造函数的prototype,从而继承原型上的方法,但是箭头函数没有prototype。因此不能使用箭头作为构造函数,也就不能通过new操作符来调用箭头函数。

14.JS数组去重

方式一:new Set()

	let arr1 = [1, 1, 2, 3, 5, 4, 1, 2]
    function unique (arr) {
      return [...new Set(arr)]
    }
    console.log(unique(arr1));

方式二:indexOf

	let arr2 = [1, 1, 2, 3, 5, 4, 1, 2]
    function unique (arr) {
      let array = []
      for (let i = 0; i < arr.length; i++) {
        if (array.indexOf(arr[i]) == -1) {
          array.push(arr[i])
        }
      }
      return array
    }
    console.log(unique(arr2));

方式三:sort 排序

	let arr3 = [1, 1, 2, 3, 5, 4, 1, 2]
    function unique (arr) {
      arr = arr.sort()
      let array = []
      for (let i = 0; i < arr.length; i++) {
        if (arr[i] !== arr[i - 1]) {
          array.push(arr[i])
        }
      }
      return array
    }
    console.log(unique(arr3));

15.this 对象的理解

在绝大多数情况下,函数的调用方式决定了 this 对象的指向

this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象

根据不同的使用场合,this 对象有不同的指向,主要分为下面几种情况

默认绑定
全局环境中定义的函数,函数内部的 this 指向 window 对象

隐式绑定
函数还可以作为某个对象的方法调用,这时 this 就指这个上级对象

new 绑定
通过构建函数 new 关键字生成一个实例对象,此时 this 指向这个实例对象

显示绑定
apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时 this 指的就是这第一个参数

箭头函数
箭头函数没有自己的 this 值,箭头函数中所使用的 this 来自于函数作用域链

箭头函数没有自己的 this 值,箭头函数中所使用的 this 都是来自函数作用域链,它的取值遵循普通普通变量一样的规则,在函数作用域链中一层一层往上找。

16.typeof 与 instanceof 区别

typeof 一般是用来判断变量是否存在,返回他的类型,其中基本数据类型 null 返回的是一个 object,null 不属于引用数据类型,typeof 除了判断 function 函数会识别,其他的引用类型输出为 object

instanceof 一般是用来判断引用数据类型,但不能正确判断基本数据类型,根据在原型链中查找判断当前数据的原型对象是否存在返回布尔类型

typeof 操作符返回一个字符串,表示未经计算的操作数的类型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

// 定义构建函数
let Car = function () {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false

区别
typeof与instanceof都是判断数据类型的方法,区别如下:

typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值

instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型

而typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

17.深拷贝浅拷贝的区别?

浅拷贝:拷贝基本数据类型为他的值,拷贝引用数据类型为地址,生成新的数据,修改新的数据会影响原数据,实际开发常用的方法有:object.assgin,扩展运算符等等

深拷贝:在内存中开辟一个新的栈空间保存新的数据,修改新数据不会影响到原数据,开发中常用的方法有:loadsh 中的_.cloneDeep()方法,JSON.stringify()

浅拷贝

function shallowClone(obj) {
  const newObj = {}
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      newObj[prop] = obj[prop]
    }
  }
  return newObj
}

在JavaScript中,存在浅拷贝的现象有:

Object.assign
Array.prototype.slice(), Array.prototype.concat()
使用拓展运算符实现的复制

深拷贝

function checkType(any) {
  return Object.prototype.toString.call(any).slice(8, -1)
}
function clone(any){
  if(checkType(any) === 'Object') { // 拷贝对象
    let o = {};
    for(let key in any) {
      o[key] = clone(any[key])
    }
    return o;
  } else if(checkType(any) === 'Array') { // 拷贝数组
    var arr = []
    for(let i = 0,leng = any.length;i<leng;i++) {
      arr[i] = clone(any[i])
    }
    return arr;
  } else if(checkType(any) === 'Function') { // 拷贝函数
    return new Function('return '+any.toString()).call(this)
  } else if(checkType(any) === 'Date') { // 拷贝日期
    return new Date(any.valueOf())
  } else if(checkType(any) === 'RegExp') { // 拷贝正则
    return new RegExp(any)
  } else if(checkType(any) === 'Map') { // 拷贝Map 集合
    let m = new Map()
    any.forEach((v,k)=>{
      m.set(k, clone(v))
    })
    return m
  } else if(checkType(any) === 'Set') { // 拷贝Set 集合
    let s = new Set()
    for(let val of any.values()) {
      s.add(clone(val))
    }
    return s
  }
  return any;
}

18.闭包

  1. 什么是闭包?函数和函数内部能访问到的变量的总和,就是一个闭包。

  2. 如何生成闭包? 函数嵌套 + 内部函数被引用。

  3. 闭包作用?隐藏变量,避免放在全局有被篡改的风险。

  4. 使用闭包的注意事项?不用的时候解除引用,避免不必要的内存占用。

为什么有闭包的这种特性:如果形成闭包,外部函数执行完后,其中的局部变量可能被内部函数使用,所以不能销毁,因此内部函数能一致访问到这些局部变量,直到引用解除。

为什么需要闭包?隐藏变量,避免放在全局有被篡改的风险。

闭包的缺点:使用时候不注意的话,容易产生内存泄漏。

19.柯里化

函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。

函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行

举例来说,一个接收 3 个参数的普通函数,在进行柯里化后,柯里化版本的函数接收一个参数并返回接收下一个参数的函数,该函数返回一个接收第三个参数的函数。最后一个函数在接收第三个参数后,将之前接收到的三个参数应用于原普通函数中,并返回最终结果

//普通函数
function fn(a, b, c, d, e) {
  console.log(a, b, c, d, e)
}
//生成的柯里化函数
let _fn = curry(fn)

_fn(1, 2, 3, 4, 5) // print: 1,2,3,4,5
_fn(1)(2)(3, 4, 5) // print: 1,2,3,4,5
_fn(1, 2)(3, 4)(5) // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5) // print: 1,2,3,4,5

对于已经柯里化后的_fn 函数来说,当接收的参数数量与原函数的形参数量相同时,执行原函数;当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数。

20.js有哪些继承方法?

js 实现继承的方式有很多种,比如 原型链继承、借用构造函数继承、组合继承、寄生继承、寄生组合继承、ES6 新增的 extends 继承
一般我开发中用的比较多的就是 原型链继承 还有 class 类函数继承。 其他的基本用得少,主要是平时看一些技术博客类文章了解过一些。

主要有几种

原型链继承
构造函数继承(借助 call)
组合继承
原型式继承
寄生式继承
寄生组合式继承

21.防抖和节流?

防抖和节流是性能优化手段

什么是防抖? 防抖:单位时间内,频繁触发事件,只执行最后一次。 防抖的主要应用场景:

搜索框搜索输入。只需用户最后一次输入完,再发送请求
手机号、邮箱验证输入检测
什么是节流? 节流:单位时间内,频繁触发事件,只执行一次。 节流的主要应用场景:

高频事件 例如 resize 事件、scroll 事件
手机号、邮箱验证输入检测
相同点:

都可以通过使用 setTimeout 来实现
降低回调执行频率。节省计算资源
不同点:

函数防抖,在一段连续操作结束后,处理回调,利用 clearTimeout 和 setTimeout 来实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

节流

function throttled(fn, delay = 500) {
  let timer = null
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}

防抖

function debounce(func, wait) {
  let timeout

  return function () {
    let context = this // 保存this指向
    let args = arguments // 拿到event对象

    clearTimeout(timeout)
    timeout = setTimeout(function () {
      func.apply(context, args)
    }, wait)
  }
}

22.递归

递归是什么? 一个函数在内部调用自己,这个函数就是递归函数。

function pow(x, n) {
  if (n == 1) {
    return x
  } else {
    return x * pow(x, n - 1)
  }
}

23.如何判断数组

// ES6中增加的数组方法
Array.isArray()

// 使用constructor判断
function isArray(arr) {
    return arr.constructor.toString().indexOf("Array") > -1;
}

function isArray(arr) {
    return arr.constructor === Array;
}

// 用instanceof判断
function isArray(arr){ 
    return arr instanceof Array; 
}

24.Promise

Promise 是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码

doSomething(function (result) {
  doSomethingElse(
    result,
    function (newResult) {
      doThirdThing(
        newResult,
        function (finalResult) {
          console.log('得到最终结果: ' + finalResult)
        },
        failureCallback
      )
    },
    failureCallback
  )
}, failureCallback)

阅读上面代码,是不是很难受,上述形成了经典的回调地狱

现在通过Promise的改写上面的代码

doSomething()
  .then(function (result) {
    return doSomethingElse(result)
  })
  .then(function (newResult) {
    return doThirdThing(newResult)
  })
  .then(function (finalResult) {
    console.log('得到最终结果: ' + finalResult)
  })
  .catch(failureCallback)

瞬间感受到promise解决异步操作的优点:

链式操作减低了编码难度
代码可读性明显增强

状态
promise对象仅有三种状态

pending(进行中)
fulfilled(已成功)
rejected(已失败)

构造函数方法

all():
Promise.all()方法用于将多个 Promise实例,包装成一个新的 Promise实例

race():
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

allSettled():
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例
只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束

resolve():
将现有对象转为 Promise对象

reject():
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

try()

25.apply、bind、call

call、apply、bind都是用来改变this指向的,

call和apply调用时候立即执行,bind调用返回新的函数。

当需要传递参数时候,call直接写多个参数,apply将多个参数写成数组。

bind在绑定时候需要固定参数时候,也是直接写多个参数。

你可能感兴趣的:(前端学习的知识点,前端,javascript)