前端面试JS总结,持续更新,闭包、原型链、异步JS总有你想看到的

459 阅读14分钟

目前格式未完全整理,内容仍在持续更新,2020.4.10...

本文均是笔者对自己参加应届生前端面试中遇到的知识点的整理,菜鸡一枚,欢迎提出任何的建议和指正!

如果对你有些许的帮助,也请评论鼓励一下作者,让我能有更多更新的动力!

使用构造函数生成新的实例并实现继承

let Cat = function(name,color){
  this.name = name,
  this.color = color
}

let sun = new Cat("Sun","red")

call apply bind

let wang = {
  name: "Wang",
  sing: function(sth){
    console.log(this.name+" sing "+sth)
  }
}
wang.sing.call(sun,'love') 

wang的sing方法借用给了sun,即在sun的context中跑sing方法

wang.sing.apply(sun,['love']) 

apply与call的不同是apply传入的参数是array,而call是一个一个传入

let sun_sing = wang.sing.bind(sun,'love') 
sun_sing()

bind是将wang的sing方法在sun上下文的运行存储下来,非立即执行

手撕call apply bind

Function.prototype.mycall = function(context){ // 参数是新的上下文,但是参数数量不定,所以没法把形式参数全部写进去
   if (typeof this !== "function"){
     throw new TypeError("not a function")
   }
   context = context||Window
   context.tempfn = this
   args = [...arguments].slice(1) //arguments是一个类数组,变成数组后切片获取第二个参数之后的所有参数
   let result = context.tempfn(...args)
   delete context.tempfn  
   return result
} 
wang.sing.mycall(sun,"love")
Function.prototype.myapply = function(context,args){
  if (typeof this !== "function"){
     throw new TypeError("not a function")
  }
  context = context||Window
  context.tempfn = this
  let result
  if(args){
    result = context.tempfn(...args) //如果args是undefined,会报错
  }else{
    result = context.tempfn()
  }
  delete context.tempfn
  return result
}
Function.prototype.mybind = function(context){
  if (typeof this !== "function"){
    throw new TypeError("not a function")
  }
  let self = this
  let args = [...arguments].slice(1)
  // F本质也是一个构造函数
  return function F(){
    if(this instanceof F){
      return new self(...args,...arguments)
    }else{
      return self.apply(context,args.concat([...arguments]))
    }
  }
}

this的指向问题

var name = "window"

var obj = {
  name:"obj",
  sayName:function(){
  console.log(this.name)
}
}
obj.sayName() //obj

普通函数体内的this的指向简而言之就是指向调用这个函数的对象,在以上代码中,obj是调用sayName函数的对象,所以this指向它。如果想要改变this的指向,可以使用上文提到的call,apply和bind。

箭头函数的this指向问题

var name = "window"

var obj = {
  name:"obj",
  sayName:()=>console.log(this.name)

}
obj.sayName() //window

箭头函数的特殊之处在于它的this指向定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。在以上代码中,即使是obj调用了sayName方法,打印出的却是全局的name,即window。这是因为定义sayName时所在的作用域是全局作用域,指向的对象自然是全局对象window。

var name = "window"

var obj = {
  name:"obj",
  sayName: function(){
    var s = ()=>console.log(this.name)
    s()
  }
 
}
obj.sayName() //obj

注意上面代码,函数s在被定义时所在的作用域为函数sayName,而这个作用域指向的对象是obj,所以这时可以输出obj。

函数防抖(debounce)与函数节流(throttle)

防抖 一个频繁被触发的函数,一段时间内只让最后一次生效,前面的不生效 使用闭包实现,每一次都相当于重启一个计时器。dom对象实际调用的是debounce生成出来的外层function,但是实际我们处理event的应该是里层的function,所以就需要我们把this和arguments都带到下一层

function debounce(fn, delay) {
  let timer // 维护一个 timer
  return function () {
    let _this = this
    var args = arguments

    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(function () {
      // 发出event的dom对象调用的是上层的func,但是真正需要的调用的是里层,但是里层由于是普通函数写法且没有被对象调用,所以this是window,拿不到event的内容,必须将上次的this保存下来作为fn的上下文
      fn.apply(_this, args)
    }, delay)
  }
}

但是使用箭头函数的写法会简单很多:

function debounce(fn, delay) {
  let timer // 维护一个 timer
  return function () {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      // 如果使用箭头函数,其实不需要再去绑定this,因为这个this就是指向箭头函数所在作用于指向的对象(相当于被透传下来了),即那个dom
      fn(...arguments)
    }, delay)
  }
}

节流 一个函数被执行后,只有再一段时间后才能再次执行 同样通过闭包实现,每次都要检查当前时间和上次运行时间的时间差是否大于设定的最大时间差

function throttle(fn,delay){
  let lastTime = 0
  return function(...args){
    let now = Date.now()
    if(now-lastTime>delay){
      fn.apply(this,args)
      lastTime = now
    }
  }
}

手撕new

let Animal = function(name, type){
  this.name = name
  this.type = type
  return {
    a:1
  }
}
function myNew(cstFn,...args){
  let newObj = {}
  newObj.__proto__ = cstFn.prototype
  let result = cstFn.apply(newObj,args)
  return (typeof result === "object"? result:newObj)
}

let cat = myNew(Animal,"cat","cats") //返回值是{a:1}

在这里需要解释的是return (typeof result === "object"? result:newObj)。在使用JS引擎默认的new关键字时,如果构造函数有返回值(不要惊讶,构造函数是一个函数,它自己有返回值也无可厚非)且是一个对象类型的值,那么new的结果并不会返回所期望的该构造函数的实例,而是会返回这个构造函数返回的对象。但是如果构造函数没有返回值或者返回值的数据类型不是对象类型的,那么便会正常获得该构造函数的实例。笔者在这里提供的Animal构造函数返回值为对象类型,所以自然不能正确得到实例,你可以自己修改或删除构造函数返回值来查看结果。因为手撕new相当于是我们对js本身new的重现,所以必须要完美模仿(copy ninja Kakashi XD)他的所有行为和返回值,这就是为什么我们在自己实现new的时候也要这么做的原因。

手撕Object.create

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};
const me = Object.create(person);
console.log(me)

me此时依然是一个空的object,他的所有相关属性和方法都要通过原型链上溯到他的__proto__,但是可以通过自己重写和新添加来直接给me对象赋予属性和方法

function myCreate(sup){  // 是使用一个对象作为参数创建他的子对象
  let newObj = {}
  newObj.__proto__ = sup
  return newObj
}
let you = myCreate(person)

手撕instanceof

沿着原型链寻找是否left是right的实例,通过不断的比较left.__proto__.__proto__...和right.prototype

function myInstanceof(left, right){
  left = Object.getPrototypeOf(left) //left.__proto__
  right = right.prototype
  while(true){  //死循环,两个出口分别是到了原型链的终点或者匹配了
    if(left === null){return false}
    if(left === right){return true}
    left = Object.getPrototypeOf(left)
  }
}

函数柯里化 Currying

函数柯里化是函数式编程的重要概念,它的意思简单来说“就是把一个多参数的函数,转化为单参数函数” 。例如 add(1, 2, 3) ==> add(1)(2)(3)

函数式编程及更具体的柯里化讲解请参考阮神文章:www.ruanyifeng.com/blog/2017/0…

那么如何把一个函数柯里化呢?

function curry(fn,initargs=[]){
    return fn.length === initargs.length? fn(...initargs):
    function(...args){
        return curry(fn,initargs.concat(args))
    }
}
function add(a,b,c){
  return a+b+c
}
let curried_add = curry(add)
curried_add(1)(2)

解释:函数fn作为对象,它拥有的属性length即为它的参数数量,当初始给定的参数数量与函数所需要的参数数量相等时,直接返回函数fn运行结果。如果初始给定参数数量少于函数fn需要的参数数量,则返回一个函数,这个函数在接受新的参数后,将新的参数与现有的参数连接起来,做为curry的新的参数递归调用curry。

闭包 类型转化 判断类型

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包 形成条件: 1. scope chain 2. higher order function

两大优势: 1. 节约内存 与垃圾回收机制有关 2. 封装私有变量并提供访问他的指定方法

类型转化 type coercion

所有的数据类型?

7种不同的数据类型:string number boolean object function symbol bigint

3种对象类型:Object Date Array

2种不含数据的类型:undefined null

JavaScript 变量可以转换为新变量或其他数据类型:

通过使用全局的String(), Number,Boolean和Date的toString()方法

通过使用全局的Number()

以上都是主动进行数据类型转化的方法,此外还有在运算逻辑中被动转化的情况,如加号+"1"转化为数字,“5”+1=>“51”,"5"-1=>4,另一个常用的便是在条件判断时,经常出现的各类型转化,

比如[] //true

[]==false //true 原因是[]和false被转化成0数字比较,

{} //true

{}==false //false原因是{}被转换成NaN,

而{} == {} //false的原因是,内存中的存储位置不同

类型判断的方法

typeof 有一些数据类型无法正确获得,如Array、Null、Date

instanceof 通过判断是否实例来判断,但是同样适用于原型链中别的类型

constructor 指向对象的构造函数,无法判断null

Object.prototype.toString方法

异步js

js是一个单线程的,在处理耗时操作时会在一定程度上阻塞整个js文件的执行,此时需要实现异步js来模仿多线程的任务执行,在遇到耗时操作时交给其他线程,函数会立即返回但是暂时获取不到运行结果,也不会阻塞之后的js脚本运行,当运行结果得到后,再执行相应的处理

1.callback

容易造成回调地狱且不能有效对异步过程中的error进行处理

2.Promise

es6新加入的更优雅的处理实现异步的方法,基本promise写法

let myPromise = new Promise((resolve,reject)=>{
  let a = 1+3 //在这里应该是一个耗时很长的异步任务
  if(a == 2){
    resolve("success")
  }else{
    reject("failed")
  }
})

myPromise.
then((msg)=>console.log(msg)).
catch((msg)=>console.log(msg))

3.async/await

以类似普通函数的方式书写异步函数

async function fetchuser(){
  const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  const data = await res.json()
  return data

}

本质:async 函数的返回值是Promise,await是等待之后的promise运行完毕后返回结果,是promise的语法糖,注意res.json()的返回值是promise

4.AJAX

如果要让用户留在当前页面中,同时发出新的HTTP请求,就必须用异步JavaScript发送这个新请求,接收到数据后,再用JavaScript更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新,可以使用fetch+promise的组合,或者fetch+async/await来替代。

example见codepen.io/monstqvq/pe…

一些数组方法

引自www.cnblogs.com/sqh17/p/852…

arr = [1,2,3,4]
arr.push(5)
arr.pop()
arr.shift() //删除头元素
arr.unshift(0) //在头部加入元素
//利用以上实现栈和队列
arr1 = ["a","b","c"]
arr1.splice(1,1,"d","e") //在index为1位置删除1个并在此加入d和e,返回值是被删除的元素组成的array
newarr = arr1.slice(1,3) //切片,不影响原数组,包前不包后
arr2 = [5,6,7,8]
//forEach 对数组进行遍历,无返回值,可用来代替for循环
arr2.forEach((value,index,array)=>{
  console.log(value+index+array)
})
//map 映射数组,返回一个新数组,不修改原数组
arr2.map((item,index,array)=>item = item*2)
//filter 过滤数组,返回一个新的满足条件的数组,不影响原数组
arr2.filter((item,index)=>item>6)
//every 判断数组里的元素是否全部满足,全部满足返回true
arr2.every((item,index)=>item<9)
//some 判断数组中是否至少一个满足的,有则返回true,全部不满足返回false
arr2.some((item,index)=>item>10)
//reduce 累加器,广义上的加不单单指加法,对数组的每个元素进行迭代,灵活使用,第二个参数是初始值
arr2.reduce((prevValue,curValue)=>prevValue+curValue,0)

还有很多,参考之前提供的链接。在以下会介绍可以用reduce方法解决的一个问题,flat打平函数

flat打平函数的实现

释放出指定深度的数组元素,即按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

developer.mozilla.org/zh-CN/docs/…

arr3 = [[1,2],[[3,[4]],5]]
// arr3.flat(1) //[1,2,[3,[4]],5]

Array.prototype.myflat = function(depth=1){
  return depth > 0? this.reduce((prev,item)=>prev.concat(Array.isArray(item)? item.myflat(depth-1):item),[]):[...this]
}

arr3.myflat(2)

在深度depth==0时,是递归的出口即base case,直接返回this的浅拷贝即可。在depth>0时,使用reduce累加器遍历数组,对每一个数组元素判断是否为数组,如果是,直接连接到已计算数组上,如果不是,对该数组元素递归调用myflat函数直到base case出现。

原型链

__proto__ prototype constructor

一个构造函数存在一个他的原型对象,用来存储方法和所有实例都通用的属性,可以通过函数的prototype显式原型属性来获得。他的原型对象有constructor属性来访问此对象的构造函数。为了在es6之前即class、extends关键字出现之前实现js的继承,使用实例对象的__proto__属性指向构造函数的原型对象的方法。由于任何一个对象都可以当做实例,也都可以作为原型对象,所以便构成了js中的原型链。

function Person(name,age){
  this.name = name
  this.age = age
}
Person.prototype.intro = function(){
  console.log("My name is"+this.name)
}

let bob = new Person("Bob",19)
console.log(bob.__proto__ === Person.prototype) 
console.log(Person.prototype.constructor === Person)
console.log(Person.prototype.__proto__ === Object.prototype)

实际上的原型链为(通过__proto__连接):

bob --> Person.prototype --> Object.prototype --> null

原型链的终点为null,所谓无生一,一生二,二生三,三生万物

**

console.log(Person.__proto__ === Function.prototype)

请注意这里的不同,Person是一个构造函数,但是它同时又是一个对象,当我们把它当作一个对象去访问他的__proto__时,他本身作为一个函数的实例自然就找到了所有函数的构造函数的原型,即Function.prototype。

2020.4.11 待续...

微任务,宏任务与Event Loop

实现了单线程JS的非阻塞运行,几乎必考的知识点

juejin.im/post/684490… juejin.im/post/684490…

在之前提到了Promise、async/await包括setTimeOut之类,它们的输出结果顺序和基本原理通过宏任务、微任务和event loop的知识都可以得到解决,以下选择一些例题进行分析,基本概念在以上提供的链接中,大佬们谈的比我要好,我就不在这里赘述了。

图片转自 juejin.im/post/684490…

const p = new Promise(resolve => {
    console.log(1);
    // throw new Error('xxx');
    resolve();
    console.log(2);

})

//throw new Error('xxx');

setTimeout(() =>console.log(6), 0)

p.then(() => console.log(5)).catch(() => console.log(7));

console.log(3)
const p1 = new Promise(() => console.log(4));

在js runtime中,有一个主线程执行栈,JS在执行全局的同步语句时会把它压栈,执行完成后出栈再执行下一条同步语句,这便是单线程的运行原理。但是在出现异步操作时,并不会被压入执行栈,而是在等待它执行结果的过程中,不阻塞同步语句的运行,一旦这个异步操作完成,它会进入到另一个队列中,等待主线程执行栈为空时,再压栈运行。那么js如何知道主栈何时为空呢?Event Loop在这里发挥了作用,他会连续的询问主栈是否为空,一旦发现为空,便从队列中压入已完成的异步任务进栈。

同样是异步任务,setTimeOut等和Promise.then()等在完成后的入栈优先级不同,setTimeOut之类为宏任务,有其自己的宏任务队列,而Promise.then()之类为微任务,有其自己的微任务队列,微任务队列的入栈优先级比宏任务队列要高。

回到此题,输出顺序为123456,原因是第一,Promise实例在创建时就会立即执行,所以其实创建promise实例是同步任务;第二,微任务的优先级比宏任务要高,所以p.then的执行要先于setTimeOut。

另外笔者在面试中还被问及了抛出错误的情况下的运行结果,如果在主线程中抛出错误,之后的语句便不会执行,结果是12Error6,如果是在Promise创建中抛出错误,则在此作用域中,抛出错误后的语句不会被执行,如果resolve()未被执行,那么p.then自然不会执行,此时需要p.catch去捕捉错误,执行相应回调函数,结果为13476。

对于async/await函数

setTimeout(() => console.log(4))

async function main() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

main()

console.log(2)

在之前异步JS中也提到了,其实async/await函数就是Promise的语法糖,是对Promise的封装,它自然也是微任务。在await之前执行的相当于promise实例声明时的语句,是同步执行的,而await之后的语句,相当于p.then中的回调,自然是异步执行的,结果是1234。

深拷贝

function deepClone(obj, map = new WeakMap()) {
  if (typeof obj === 'object') {
    let cloneObj = Array.isArray(obj) ? [] : {}
    if (map.get(obj)) {
      return map.get(obj)
    }
    map.set(obj, cloneObj)
    for (let key in obj) {
      cloneObj[key] = deepClone(obj[key], map)
    }
    return cloneObj
  } else {
    return obj
  }
}

并非是最完美全面的写法,解决了数组、对象的深拷贝,以及存在循环引用的问题(通过一个WeakMap来记录已经拷贝过的对象和它拷贝出来的值,使用WeakMap的好处是键是弱引用,可以被垃圾回收)。详细版本参考:segmentfault.com/a/119000002…

深比较

deepEqual = (target, obj) => {
  if (target === obj) {
    return true
  } else if (typeof target == 'object' && target != null && typeof obj == 'object' && obj != null) {
    if (Object.keys(target).length !== Object.keys(obj).length) {
      return false
    }
    for (let key in target) {
      if (obj.hasOwnProperty(key)) {
        if (!deepEqual(target[key], obj[key])) return false
      } else {
        return false
      }
    }
    return true
  } else {
    return false
  }
}