JavaScript系列干货-上

1,274 阅读1小时+

1,原型对象与原型链

  • 原型对象:函数有一个属性prototype,该属性就是原型对象,该属性的作用是为该函数实例化的对象用来共享某些属性。

  • 原型链:实例对象拥有属性__proto__,该属性指向其构造函数的原型对象,当我们访问当前实例对象某个属性,当前实例对象中无,就会通过__proto__访问其原型对象查找该属性,如果还没查找到,就回通过当前原型对象的__proto__继续查找,一直查找到Object原型对象,这一系列的__proto__串联就是原型链。

  • 深入理解原型链:

        function fn() { }
        // 1,fn作为一个函数, 可以由 new Function() 生成,fn的原型指向Function的原型对象
        fn.__proto__ === Function.prototype
        // 2,Function 本身作为构造函数,是个函数,函数就可以由 new Function() 生成,Function的原型指向Function的原型对象
        Function.__proto__ === Function.prototype
        // 3,Object 本身作为构造函数,是个函数,函数就可以由 new Function() 生成,Object的原型指向Function的原型对象
        Object.__proto__ === Function.prototype
        // 4,Function 原型对象是个对象(虽然 typeof Function === 'function'),对象由 new Object()生成,Function原型对象的原型指向Object的原型对象
        Function.prototype.__proto__ === Object.prototype
        // 5,Object原型对象的原型规定为null
        Object.prototype.__proto__ === null
    

2,继承的实现

  • 1,借用构造函数继承(经典继承):子构造函数内部调用父构造函数并改变父构造函数this指向子构造函数创建的实例对象

        function Parent(color) {
            this.color = color
        }
        function Child(color, name) {
            this.name = name
            Parent.call(this, color) // 借用父级构造函数实现继承
        }
    
    • 借用构造函数继承存在的问题:无法继承父构造函数原型对象中的定义的属性方法
  • 2,组合继承(伪经典继承):经典继承存在无法继承父构造函数原型对象上的属性方法,伪经典继承在经典继承的基础上实现对父构造函数的原型对象继承,从而解决经典继承存在的问题。

    • 1,使用经典继承,继承父构造函数内部属性
    • 2,子构造函数原型对象指向父构造函数实例对象,通过原型链继承父构造函数原型对象上的属性方法
    • 3,设置子构造函数原型对象的构造器指向子构造函数
    function Parent(color) {
        this.color = color
    }
    Parent.prototype.show = function () {
        console.log(this.color);
    }
    
    function Child(color, name) {
        this.name = name
        // 1,借用构造函数继承
        Parent.call(this, color)
    }
    // 2,子构造函数原型对象指向父构造函数实例对象,通过原型链继承父构造函数上的原型方法
    Child.prototype = new Parent()
    // 3,子构造函数原型对象设置其构造器
    Child.prototype.constructor = Child
    
  • 2.5,Object.create:在介绍下面继承之前先讲解一下Object.create方法使用,Object.create(prototype,propDescriptors)创建返回一个对象,该对象__proto__指向第一个参数(prototype),同时该方法第二个参数(propDescriptors)非必传,其作用是为Object.create创建的对象添加一个获或多个内部属性,且propDescriptors默认设置的为数据属性,如果仅初始化value,则剩下的数据属性(enumerable,writable,configurable)默认为false

    const prot = { school: 'AHUT' }
    const res = Object.create(prot, {
        'name': { value: 'Linda' },
        'age': { value: 19 }
    })
    console.log('res:', res);
    console.log('res.__proto__:', res.__proto__);
    console.log('res_prop_descriptors', Object.getOwnPropertyDescriptors(res));
    

    image.png

  • 3,寄生式继承:即利用Object.create()方法实现当前对象的原型指向需要继承的对象,同时在当前对象上挂载自己的属性方法。

    const parent = { name: 'John' }
    // child对象原型指向parent,实现对parent属性的继承
    const child = Object.create(parent)
    // 继承后的child对象挂载自己的属性
    child.color = 'red'
    
  • 4,寄生组合式继承 :最常用的继承方式。

    • 1,使用经典继承继承父构造函数内部属性
    • 2,使用寄生式继承继承父构造函数原型对象
    function Parent(color) {
        this.color = color
    }
    Parent.prototype.show = function () {
        console.log(this.color);
    }
    
    function Child(name, color) {
        this.name = name
        //  1,借用构造函数继承父构造函数内部属性
        Parent.call(this, color) 
    }
    // 2,寄生式继承继承父构造函数原型对象,不要忘记指定构造器指向子构造函数
    Child.prototype = Object.create(Parent.prototype,{constructor:{
        configurable:true,
        enumerable:false, // 构造器对象是不可枚举的
        writable:true,
        value:Child
    }})
    
    • 寄生组合式继承相较于组合式继承更优美,同时组合式继承执行了两次父构造函数,而寄生组合式继承执行了一次。
  • 4.5,关于寄生组合继承可能你会疑惑的两个地方

    • 1,为什么 Child.prototype 不直接赋值为 Parent.prototype,即 Child.prototype = Parent.prototype?
      • 首先如果这么做,Child.prototype.constructor将指向Parent,显然不合适
      • 其次,我们再向Child.prototype添加共享属性,也会改变Parent.prototyp,因为它们是同一个对象
    • 2,为什么 Child.prototype = Object.create(...) 中的constructor数据属性要这样设置 configurable:true, enumerable:false, writable:true, value:Child
      • configurable:true:保证 Child.prototype.constructor是可以配置的
      • enumerable:false:保证对Child对象属性遍历过程中不会把constructor给遍历出来
      • writable:true:Child.prototype.contructor当然应该是可重新给定值的
    • 3,关于这两个问题,另一方面也保证了我们的寄生组合继承最大程度与class继承的结果相同

3,数据类型

  • 基本类型:number string boolean null undefined symbol
  • 引用类型:object function array date math...

4,判断数据类型方法

  • typeof :一般用于判断基本类型。

    • 基本类型除了typeof(null) === 'object',其他基本类型都可以正确判断

    • 引用类型中除了typeof(()=>{}) === 'function',可以正确判断,其它引用类型判断结果都是'object'

  • instanceof: 一般用于判断引用类型,instanceof 左边是需要判断的值,右边必须是一个可执行的引用类型(函数)。

    • instanceof判断依据是 左边__proto__是否与右边prototype指向同一个对象,不是同一个对象左边__proto__会继续顺着原型链即左边__proto__.__proto__与右边prototype_继续对比是否指向同一个对象,如果到原型链顶部还不是同一个对象返回false,否则返回true。

    • instanceof左边如果是基本类型值,不管右边是什么引用类型,总返回false

  • Object.prototype.toString.call(): 精确判断当前类型,但也有缺陷,比如判断1与new Number(1)结果相同,但实际一个是基本类型,一个是引用类型。如下:

    image.png

5,var,直接声明,let,const 四种声明方式区别

  • 1,全局环境中四种变量声明方式区别:这里以浏览器环境举例,其全局对象是window。

    • var a = 1:全局环境中var声明变量会挂在全局对象window上,所以除了直接访问a还可以使用window.a获取,注意不可以使用delete a 删除var声明挂载在全局对象中的变量(因为var声明变量数据属性configurable为false)

    • a = 1 :全局环境中(其实不论在还是在函数中或者是块中声明)直接声明的变量,a都会挂在window上,所以可以直接访问a,也可以window.a 去访问,同时还可以使用delete a 直接删除全局对象上的a(因为直接声明的变量数据属性configurable为true),注意直接声明的变量是在代码执行到直接声明变量那一行才将该变量挂到全局对象上。所以直接声明的变量不存在变量提升。

    • let a = 1/const a = 1:全局环境中使用let/const声明的变量/常量,可以直接访问,但不可以使用window.a访问,因为let/const声明的变量会放在全局作用域下的一个Script作用域中,所以window.a访问不到a。

    • 只有在全局作用域中会为let,const单独创建Script作用域,且全局中,直接声明,var声明以及函数声明(function(){})属于全局作用域,挂在全局对象上。

  • 2,局部作用域(块作用域,函数作用域)中四种声明:

    • 函数作用域中不会为let,const单独创建Script作用域,且函数作用域var,let,const声明以及函数声明都属于当前函数作用域。

    • 块作用域({})中不会为let,const单独创建Script作用域,块作用域中let,const声明属于当前块作用域。而var声明不属于块作用域,属于其为函数或者全局的最近父作用域中

    • 函数作用域与块作用域中的直接声明在代码执行到此处时直接声明会挂载到全局作用域中

    image.png

  • 2,是否存在变量提升

    • var存在变量提升

    • 直接声明不存在变量提升,只有执行到直接声明那一行代码,才会将直接声明的变量绑定到全局对象上

    • let,const不存在变量提升,且let,const存在暂存性死区,声明前访问会报错。

    • 变量提升:

      • 1,函数|块|全局代码 执行之前会创建其执行上下文中的变量对象

      • 2,变量对象中包括当前环境(函数|块|全局)中所有变量声明function形式的函数声明,如果是函数环境,还包括函数中的 形参以及 arguments对象(tips:箭头函数无arguments对象)。

        • 2.1:函数声明:变量对象创建时,其函数声明会完成初始化,即函数声明所对应的函数值。

        • 2.2:形参:变量对象创建时,形参会完成初始化,初始化值即传入函数的参数值,未传入对应形参的参数,则形参初始化为undefined。

        • 2.3:arguments对象:变量对象创建时,arguments会完成初始化,初始化值即传入函数的参数值,未传入对应形参的参数,则形参初始化为空的arguments对象。

        • 2.4:变量声明:变量对象创建时,变量声明中的仅var声明会进行初始化,且初始化值为undefined。而let/const声明不会初始化,所以此时let/const声明处于未初始化状态

      • 3,执行上下文创建完毕(变量对象创建完毕),开始执行函数代码,在未执行到var声明的变量之前使用该变量,该变量值为undefined(因为创建执行上下文中变量对象时对var声明初始化为undefined),当执行到var声明那一行代码,var声明将完成赋值,所以var声明之后访问到的是var声明赋值后的值。

      • 4,而在代码未执行到let/const之前访问该变量/常量会抛错(暂存性死区),因为变量对象中let声明与const声明并未初始化导致,当代码执行到let/const声明,将完成对let/const声明初始化赋值,完成let/const赋值后,我们便可以访问let与const声明值。

  • 3,是否可以重复声明或者修改

    • var可以重复声明,也可以修改

    • 直接声明变量可以重复声明,也可以修改

    • let不可以重复声明,但可以修改

    • const不可以重复声明也不可以修改,声明时必须赋值。 当然const定义的引用类型可以修改其内容,只要不修改引用类型地址即可。

6,new 过程

  • 创建一个新对象
  • 该对象的__proto__指向构造函数prototype
  • 构造函数的this指向新对象
  • 执行构造函数中代码(初始化实例对象属性)
  • 构造函数return引用类型则构造函数返回该引用类型,否则返回创建的对象

7,call实现(apply实现一样,只是参数不同)

 Function.prototype.call_ = function (that, ...args) {
    // 如果需要绑定的this是 null/undefined ,那么该函数最终this指向全局对象
    // 如果需要绑定的this是其它基本类型,那么该函数最终this指向基本类型的包装类型
    // 如果需要绑定的this是引用类型,那么this指向该引用类型
    that = [null, undefined].includes(that) ? window : Object(that)
    // 声明一个Symbol属性,防止that上挂属性时冲突
    const s = Symbol('s')
    that[s] = this
    // 利用普通函数的this指向特性,将调用call的函数挂载到传入参数that上,并传入执行(that[s](...args))获取改变this后执行结果 
    const res = that[s](...args)
    // 删除因改变this指向而挂的属性
    delete that[s]
    // 返回结果
    return res
}

8,bind实现(bind返回的函数是可以new 调用的)

处理bind函数需要注意 bind返回的函数除了传参正常执行,还可以被new, 被new的时候,1,返回的对象是 (bind返回的函数) 的实例,但返回对象的原型指向于 (调用bind的函数) 的原型对象。2,且new (bind返回的函数),(bind返回的函数)内部this指向将输出的实例,而不是bind绑定时所绑定的this。

Function.prototype.bind_ = function (that, ...args1) {
    // 1,将需要绑定的this处理, null/undefined => window 基本类型=》基本类型的包装类型 引用类型则不做处理
    that = [null, undefined].includes(that) ? window : Object(that)
    // 2,获取bind_函数this,即调用bind_的函数
    const this_ = this
    // 3,准备一个fn函数返回出去,执行该函数(fn)则 调用bind_的函数 它的this指向that,如果是new该函数(fn),则调用bind_的函数 它的this指向fn作为构造函数new出的实例
    function fn(...args2) {
      return this_.call(this instanceof fn ? this : that, ...args1, ...args2)
    }
    // 4,fn的原型也要改变,因为如果是new fn ,返回的实例原型指向 调用bind_的函数 的原型对象
    fn.prototype = this_.prototype
    // 5,返回fn
    return fn
}

9,闭包

  • 什么是闭包:

    • 广义上的闭包:能够访问自由变量的函数即闭包。

      • 自由变量:在当前作用域中使用但非当前作用域内声明的变量即自由变量。
    • 常说的闭包:A函数内声明并返回的B函数,B函数即闭包,B函数可以访问到A函数作用域链中所有的变量。

    • 闭包常用来缓存数据。

  • 闭包原理:拿父函数内部创建子函数,并返回子函数举例

    • 1,当调用父函数的时候(注意是调用而不是创建),会先创建父函数的执行上下文压入执行上下文栈中,此时函数内代码还未执行。父元素的执行上下文中包括:

      • 1.1,创建父函数的变量对象(变量对象包括argument对象,形参初始化,函数声明初始化,变量声明初始化),

      • 1.2,确定父函数的this指向

      • 1.3,创建父函数的作用域链(父作用域链的创建是由父函数外层的作用域链与父元素的变量对象组合而成),函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。

    • 2,父函数执行上下文创建完毕,父函数代码执行,当执行到子函数创建的时候(注意是函数创建而不是函数调用),因为js采用的是静态作用域,即函数创建的时候其作用域查找范围就已经确定,所以子函数的作用域目前就是父元素的作用域链的查找范围,这个作用域链被保存在内部的[[scope]]属性中,子函数创建完毕,返回子函数。

    • 3,当调用子函数,依然会先创建子函数执行上下文压入执行栈,其子函数执行上下文内的作用域链将复制先前创建子函数生成的[[scope]]与当前子函数的变量对象,生成子函数的作用域链,子函数执行上下文创建完毕,代码执行遇到变量读写将会顺着子函数的作用域链查找。

    • 4,因此闭包函数会携带父函数的作用域链,因此即使父函数执行完毕,对应执行上下文出栈(因为闭包存在,父函数的作用域链对象其实一直存在),子函数也可以顺着作用域链访问到父函数中的变量。

  • 注意:因为闭包会携带包含它的函数的作用域,因此会比其他函数占用跟多内存,所以过度闭包使用会导致内存占用过多,慎用。

10,this指向

  • 非构造函数内时,普通函数谁调用指向谁,没人调用指向全局对象。

  • 构造函数内部this指向实例化的对象。

  • 非构造函数内时,箭头函数this捕获距离当前箭头函数最近的 父级普通函数(父级非箭头函数) 的 执行上下文中的this 作为自己的this,如果不存在这种父级非箭头函数,则箭头函数的this将指向全局对象。

  • 构造函数内部的箭头函数this指向当前构造函数实例化出来的对象。

11,什么是Symbol

  • 1,Symbol数据:用来表示一种独一无二的值

  • 2,Symbol数据用途:对象属性名很容易发生冲突,即重名,而Symbol数据标识独一无二的值,所以我们可以使用Symbol数据作为对象属性避免属性重名。

  • 3,Symbol数据创建方式:Symbol(des)

    • 其中des参数属性可选,一般是string类型数据,主要用来描述Symbol数据,否则一堆Symbol()也不好辨认谁是谁。

    • 当然des参数也可以是引用类型,不过当是引用类型会调用该引用类型toString方法将其转为字符串作为当前Symbol数据的描述。

    • 因为des参数仅是当前Symbol数据的描述,所以即使两个Symbol数据的des相同,它们也不相同。

    • 如果希望获取Symbol数据的描述值可以通过Symbol('tree').description获取

    • 以下是des传入几种不同类型的描述参数生成的Symbol数据造型

      image.png

  • 4,获取对象中的Symbol值属性:可以通过Object.getOwnPropertySymbols(obj)获取obj中所有Symbol值属性(注意:原型上的Symbol属性不会被该方法获取到)。如果期望获取obj中所有Symbol值属性与非Symbol值属性可以通过Reflect.ownKeys(obj)获取。注意:使用Object.keys/values/entries,for...in并不能作用到对象上的Symbol属性。

  • 5,Symbol.for(des)方法:

    • Symbol.for(des)接受一个字符串des作为参数,然后在全局登记的Symbol中寻找有没有描述参数为des的Symbol值,如果有,返回该值,如果没有则在创建一个描述参数为des的Symbol值(并在全局环境中对该Symbol值进行登记)

    • 与直接使用Symbol(des)创建Symbol值区别是:Symbol.for(des)创建的Symbol值的同时会在全局环境中对该值进行登记,而Symbol(des)则仅创建该Symbol值而不会对该值登记。而Symbol.for是对全局环境中登记过的Symbol搜索,所以对于直接使用Symbol(des)创建的Symbol值是搜索不到的。

    • Symbol.for(des)创建的Symbol值用途:可以对同一个描述参数的Symbol重复使用,比如我们调用Symbol.for('cat')30次,仅创建一个Symbol值,而调用30次Symbol('cat')会创建30个Symbol值。

  • 6,Symbol.keyFor(sym)方法:

    • Symbol.keyFor(sym)接受一个Symbol值sym作为参数,如果sym是登记Symbol值,该方法将返回其描述参数,如果非登记Symbol值,则返回undefined。
  • 7,Symbol值注意事项:

    • Symbol不能参与运算,会报错

    • Symbol值可以显式转换成字符串

      String(Symbol('sym'))   //  "Symbol(sym)"
      Symbol('sym').toString()//  "Symbol(sym)"
      
    • Symbol值可以转换成布尔值但不可以转换成Number类型

      Boolean(Symbol('sym')) // true
      Number(Symbol('sym'))  // TypeError
      
    • Symbol值作为属性名时不能使用点运算符,应该放在[]中完成属性挂载

13,介绍一下Set,WeakSet

  • Set:Set是一种数据结构,类似于数组,但是没有重复值。Set接收具有遍历器对象的数据结构作为参数,比如数组,字符串。我们经常会使用Set对数组进行去重。注意:Set去重原理类似于精确等于===,但是与精确等于有一点细微区别,NaN===NaN 为false,但是Set认为NaN等于NaN,+0===-0 为false,但是Set认为+0等于-0,所以中出最多只会出现一个NaN或者0
  • Set上的方法
        const set = new Set([0, 1])
        // 1,获取set成员数量,类似于数组length
        const size = set.size
        // 2,add用于添加set成员
        set.add(2)
        // 3,delete用于删除set成员
        set.delete(0)
        // 4,has确定set中有没有当前值
        const has1 = set.has(1) // has1 === true
        // 5,keys返回set成员所有键名(是个遍历器对象,不是数组),由于set内键名与键值为同一个值,所以keys 与values军返回所有set成员
        const keys = set.keys()
        // 6,values返回set成员所有键值(是个遍历器对象,不是数组),由于set内键名与键值为同一个值,所以keys 与values军返回所有set成员
        const values = set.values()
        // 7,entries返回set内所有成员遍历器对象,包括键名与键值,由于set内键名与键值为同一个值,所以遍历器对象键名键值相同
        const entries = set.entries()
        // 8,forEach 遍历set每个成员,回调函数中key、value相同,即键名键值
        set.forEach(( value,keys) => {
            console.log(key, value); // 1,1,2,2
        })
        // 9,set数据结构具有遍历器接口,所以也可以使用for of遍历,当然...扩展运算符也可以
        for (const value of set) {
            console.log(value) // 1,2
        }
        // clear清除set内所有成员
        set.clear()
    
  • WeakSet:WeakSet也是不重复数据的集合,但是它的成员只能是对象,且成员均是弱引用,弱引用见下面WeakMap。

14,介绍一下Map

  • 对象本质是键值对的集合,键只可以是字符串或者Symbol类型,而map也是键值对集合,但是它的键可以为任何数据类型。
  • map上的方法
        // 1,new 创建map对象 传入键值对['a',1],['b', 2]
        const map = new Map([['a', 1], ['b', 2]])
        // 2,get 方法获取map指定键对应值
        const a = map.get('a') // a = 1 
        // 3,set 向map中添加新的键值对,键相同则覆盖原有键值对
        map.set('a', 0)
        // 4,keys获取map中所有key,返回的是个迭代器对象,可以使用扩展运算符转成数组
        const keys = map.keys() // keys = ['a','b']
        // 5,values获取map中所有value,返回的是个迭代器对象,可以使用扩展运算符转成数组
        const values = map.values() // keys = [0,2]
        // 6,entries获取map中所有键值对,返回的是个迭代器对象,可以使用扩展运算符转成数组
        const entries = map.entries()
        // 7,forEach 遍历map每个成员,回调函数中key,value对应map键值对
        map.forEach((value, key, map) => {
            console.log(key, value); // a 0 , b 2
        })
        // 8,map 具有遍历器接口,所以也可以使用for of遍历,当然扩展运算符也可以
        for (const iterator of map) {
            console.log(iterator); // ['a',0],['b',2]
        }
        // 9,has判断map中是否有当前键
        const hasA = map.has('a') // hasA = true
        // 10,size判断map中成员数量(类似数组length)
        const size = map.size // size = 2
        // 11,delete 删除map中指定key的键值对
        map.delete('a')
        // 12,clear 删除map内所有键值对
        map.clear()
    

15,Map与WeakMap的区别,WeakMap的用途。

  • Map与WeakMap的区别:
    • WeakMap的键名只能是引用类型,Map任意

    • WeakMap的键名(仅键名是弱引用,键值如果是对象仍然是强引用)所指向的对象行为为弱引用,不计入垃圾回收机制,Map强引用,计入。

    • ps:因为WeakMap的键名存在不可预测,这和垃圾回收机制有关,比如这一刻获取键名,下一刻垃圾回收机制运行,键名没了,导致js代码出现不可确定性,所以WeakMap没有遍历操作(keys(),values(),entries()),size属性以及clear函数。而map有这些属性与方法。

      const wm = new WeakMap()
      // key 对 { k: true } 引用,{ k: true }被引用次数一次
      let key = { k: true }
      let value = { v: true }
      // wm 对 { k: true } 引用,但是由于是弱引用,{ k: true }被引用次数还是一次
      wm.set(key, value)
      // 解除 key 对 { k: true } 引用,{ k: true }被引用次数为零,{ k: true }将被垃圾回收机制处理掉
      key = null   
      
  • 用途:那些容易忘记消除对象引用的地方,比如dom节点上绑定一些信息,我们可以将dom节点作为键名,信息存入键值,当dom节点删除,weakmap中的dom及信息将被垃圾回收清除。如果我们使用map做这件事,我们可能需要map.delete(dom)消除dom引用。

16, 箭头函数和普通函数的区别?

  • 1,箭头函数this捕获当前上下文this,普通函数this大部分是谁调用指向谁
  • 2,箭头函数不能作为构造函数,普通函数可以
  • 3,箭头函数无arguments,普通函数有
  • 4,箭头函数无原型属性(即prototype,其prototype属性为undefined),普通函数有。
  • 5,箭头函数无法通过call,apply,bind绑定this,普通函数可以
  • 6,箭头函数不能使用yield,不能作为generator函数,普通函数可以

17,ES6的class

class Animal {
    constructor(color) {
        // ------- 实例属性(实例对象中的属性)
        this.color = color
    }

    // ------- 原型方法(原型上的方法)
    // 原型方法run,当然['run']() { console.log('run') }这种表达式方法名也是合法的。
    run() { console.log('run') }
    // 然原型上也可以声明generator函数,当然其它自定义generator函数也是OK的。
    *[Symbol.iterator]() { yield 1; yield 2; }

    // ------- 静态方法(类身上的方法)
    // 使用static 创建静态方法 静态方法中this指向Animal类 并不是指向Animal的实例对象 
    static showClassName() { console.log(this.name); }

    // ------- 静态属性(类身上的属性)
    // static除了创建静态方法也可以创建静态属性,即类身上的属性 这种操作是提案 提案之前创建静态属性都是直接在类上挂静态属性,像这样:Animal.type = 'like-cat'
    static type = 'like-cat'  
}
  • 1,class可以看作ES5继承的语法糖,它的绝大部分功能ES5都可以做到,新的class写法只是让对象原型写法更加清晰,更像面向对象编程语法而已。

  • 2,class 内部所定义的普通方法(非静态方法)都存在于prototype对象上,当然我们也可以在继续向prototype上添加新的方法。Animal.prototype.eat = funtion(){ console.log('eat') }

  • 3, prototype上的constructor指向类本身,且type of Animal === 'function',这与ES5是一致的。

  • 4,类内部所定义的所有方法都是不可枚举的,这与ES5行为不一致。Object.keys(Animal.prototype) // []

  • 5,类内部constructor方法可以省略(省略时,一个空的constructor方法将被默认添加),且constructor方法默认返回实例对象(即this),但如果显示指定return 引用类型,则new 该类返回的就是显示指定所返回的引用类型数据,如果return 基本类型,返回当前类实例对象,这一点与ES5是一致的。

  • 6,类必须使用new 调用,而ES5中除了new 调用还可以直接执行构造函数。且ES5中构造函数存在状态提升,而ES6中类不存在状态提升,必须先创建类,再使用。

  • 7,类/构造函数 的实例对象的原型(__proto__)指向 类/构造函数 的原型对象(prototype),我们可以使用 new Animal().__proto__获取到 类/构造函数 的原型对象,但是由于__proto__并不是js语言特性,只不过现在的浏览器js引擎提供了这个属性,所以为了避免代码对环境产生依赖,我们可以使用JS提供的Object.getPrototypeOf方法获取实例对象的原型。(虽然我们可以通过实例对象获取到 类/构造函数 的原型对象,但不建议通过这样的操作去修改 类/构造函数 的原型对象,因为这会影响所有 类/构造函数 的实例对象,而且修改了 类/构造函数 的原始定义。)

  • 8,类的内部可以使用get/set关键字,其作用即对原型对象设置属性的存取值函数,拦截该属性的存取行为,如下code,本质是在原型对象上添加prop属性,且设置了prop的描述属性中的get/set方法。

     class MyClass {
        get prop() {
            console.log('getter');
        }
        set prop(value) {
            console.log('setter');
        }
    }
    console.log('MyClass Prototype:', Object.getOwnPropertyDescriptors(MyClass.prototype));
    

image.png

  • 9,类也可以使用表达式形式定义,即如下是合法的,且表达式定义的类可以省略类名(即 const Silk = class Milk {} 中Milk可以省略),同时,该方式的类声明,Milk只可以在类内部使用,类外部只能使用Silk。

     const Silk = class Milk {
        getClassName() {
            console.log('Milk.name:', Milk.name); // Milk
            console.log('Silk.name:', Silk.name); // Milk
        }
    };
    
  • 10,类内部默认就是严格模式,这与ES5构造函数不一致

  • 11,类内部可以使用static 声明类的静态方法,静态方法不像原型方法被类的实例继承,静态方法只通过类进行直接调用(像这样,Animal.showClassName()),且类的静态方法this指向类,而不是类的实例对象,且静态方法可以被子类继承,子类继承父类静态方法,静态方法内部this将指向子类。

  • 12,类构造器中的实例属性也可以抛弃构造器直接在类最顶层书写,如下,两种类效果一样的。(我们经常会在react中见到state这样直接在组件类最顶层书写)

    class Cat {
        constructor(color) {
            this.color = 'black-white'
        }
    }
    class Cat {
        color = 'black-white'
    }
    
  • 13,我们也可以直接在类内部使用static声明静态属性。

  • 14,类内部也可以声明私有属性与私有方法(即只能在类内部访问的属性方法),这也是存在提案中,即私有属性、私有方法前面加#号,如果私有属性或方法前面加上了static,那么就是静态私有属性方法,即只能在类内部通过类访问到的属性或方法。

  • 15,new.target,new.target一般用于构造函数/类中,如果当前构造函数/类通过new 调用,那么new.target指向构造函数/类本身,如果在构造函数直接执行,那么new.target为undefined

17,ES6的继承使用ES5完全实现

这里准备了ES6继承实现 与 ES5完全实现ES6继承的代码,如下,可以看到ES5对ES6继承基本就是靠组合寄生继承来实现,当然还是有些小小区别,下面会说。

  • 第一种:ES6 子类继承自定义的父类,如下即子类Cat继承自定义父类Animal

    • ES6:

      // 父类 Animal
      class Animal {
          // Animal 构造器
          constructor(color) { this.color = color }
          // Animal 静态属性
          static location = 'Earth'
          // Animal 静态方法
          static look = function () { console.log('look ~') }
          // Animal 原型方法
          run() { console.log('run ~') }
      }
      // 子类 Cat 继承自Animal
      class Cat extends Animal {
          // Cat 构造器
          constructor(color, name) { super(color); this.name = name }
          // Cat 静态属性
          static nickName = 'cute'
          // Cat 静态方法
          static jump = function () { console.log('jump') }
          // Cat 原型方法
          eat() { console.log('eat') }
      }
      
    • ES5完全实现:

      "use strict";
      
      // 1,声明父类Animal
      function Animal(color) {
          this.color = color;
      }
      // 2,向Animal.prototype中挂载原型方法
      Object.defineProperty(Animal.prototype, 'run', { value: function run() { console.log('run ~') }, enumerable: false, configurable: true, writable: true });
      // 3,向Animal函数本身挂载 静态属性 与 静态方法
      Object.defineProperty(Animal, 'location', { value: 'Earth', enumerable: true, configurable: true, writable: true });
      Object.defineProperty(Animal, 'look', { value: function () { console.log('look ~') }, enumerable: true, configurable: true, writable: true });
      
      
      // 4,声明子类Cat
      function Cat(color, name) {
          var instance
          // 5,父类修饰子类的this对象,挂载父类中的实例属性到this对象中
          var result = Animal.call(this, color);
          // 6,如果父类修饰子类的this对象后返回了引用类型,那么该引用类型作为子类未来的实例对象,否则子类this对象作为子类未来的实例对象。
          instance = result instanceof Object ? result : this
          // 7,子类的实例对象上挂载子类中的实例属性
          instance.name = name;
          // 8,返回子类实例对象
          return instance;
      }
      // 9,子类继承父类上的静态属性与静态方法
      Object.setPrototypeOf(Cat, Animal)
      // 10,和寄生组合继承中子类继承父类原型对象一模一样
      Cat.prototype = Object.create(Animal.prototype, { constructor: { value: Cat, writable: true, configurable: true } });
      // 11,向Cat.prototype中挂载原型方法
      Object.defineProperty(Cat.prototype, 'eat', { value: function eat() { console.log('eat'); }, enumerable: false, configurable: true, writable: true });
      // 12,向Cat函数本身挂载 静态属性 与 静态方法
      Object.defineProperty(Cat, 'nickName', { value: 'cute', enumerable: true, configurable: true, writable: true });
      Object.defineProperty(Cat, 'jump', { value: function () { console.log('jump'); }, enumerable: true, configurable: true, writable: true });
      
    • 注释1,注释4:声明父类Animal,声明子类Cat,没什么说的。

    • 注释2,注释11:向原型对象中挂载原型方法的同时设置了原型方法的描述属性,而ES5是直接挂载 Constructor.prototype.protoKey = protoValue,二者区别在于ES6中原型方法的描述属性enumerable为true,而ES6中原型方法enumerable为false。

    • 注释3,注释12:向构造函数本身挂载静态属性与静态方法,且描述属性都为true。这与直接Constructor.staticKey = staticValue 挂载方式没区别。

    • 注释9:将子构造函数的__proto__指向父构造函数,这样就可以在子类中通过原型链访问到父类上定义的静态属性与方法。

    • 注释10:与寄生组合继承中子构造函数继承父构造函数的原型对象操作一样。

    • 注释5,注释6:注释5与注释6就是ES6继承中super(props)的行为,先将借用父构造函数修饰子类this对象,再根据父类修饰完毕子类this对象返回值判定未来返回出去的实例对象是子类this对象还是父类返回的引用类型。

    • 注释7:向实例对象中挂载子类的实例属性。看注释567,我们可以发现注释7如果放到到注释56之前,就会报错,因为此时子类输出的实例对象是通过注释56创建出来的,所以必须注释56先执行,再执行注释7,这也就是为什么ES6继承中super(props)必须放在构造器函数顶部。

    • 注释8:返回修饰完全的子类实例对象。

  • 第二种:ES6 子类继承原生类,如下MyArray继承自Array类

    • ES6:

      class MyArray extends Array {
          constructor(color, ...args) {
              super(...args);
              this.color = color
          }
      }
      
    • ES5完全实现:

      // 1,声明子类MyArray
      function MyArray(color, ...args) {
          var instance
          // 2.1,取color后面的参数交给Array执行返回一个数组
          // 2.2,将该数组的原型(__proto__)设置为MyArray的原型对象,并返回该数组给result
          var result = Object.setPrototypeOf(Array(...args), MyArray.prototype);
          // 3,result这里是之前返回的数组,所以MyArray将要返回的实例对象instance就是result
          instance = result instanceof Object ? result : this
          // 4,向将要返回的实例对象上挂载color属性
          instance.color = color;
          // 5,返回该实例对象
          return instance;
      }
      // 6,MyArray原型对象的原型指向Array的原型对象(继承Array原型方法)
      MyArray.prototype = Object.create(Array.prototype, { constructor: { value: MyArray, writable: true, configurable: true } });
      // 7,MyArray的原型指向Array(继承Array上的静态属性,方法)
      Object.setPrototypeOf(MyArray, Array);
      
      • 注释2.1,注释2.2:如果你看过上面子类继承自定义父类,你会发现与子类继承原生类最大的区别就在注释2.1,注释2.2。子类继承自定义父类中,是将子类this对象交给父构造函数修饰返回,而子类继承原生类中是交给原生构造函数修饰实例属性,同时其返回值的原型设置为子类原型对象。
  • 上面的ES6转ES5都是将babel转义后的代码剔除不重要的代码,保留核心实现。其中子类继承原生构造函数其实中间还有个中间函数,不过被我简化了成上面注释2一行代码,下面是该部分未被简化的实现,有兴趣的可以看一看,建议按照注释顺序阅读代码。

    "use strict";
    // 3,Wrapper:在子类与父类Array中间使用Wrapper连接,即子类继承Wrapper,Wrapper继承Array
    function Wrapper() {
        // 4,下面两行代码作用相当于下面注释代码,即创建一个数组,并传入参数
        var Constructor = Function.bind.call(Array, null, ...arguments);
        var instance = new Constructor();
        // var instance = Array(...arguments)
        // 5,设置数组原型为子类原型对象
        Object.setPrototypeOf(instance, MyArray.prototype);
        // 6,返回该数组
        return instance;
    }
    // 7,Wrapper原型对象的原型设置为Array原型对象(即Wrapper继承Array的原型方法)
    Wrapper.prototype = Object.create(Array.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } });
    // 8,Wrapper原型设置为Array原型(即Wrapper继承Array上静态方法与属性)
    Object.setPrototypeOf(Wrapper, Array);
    
    // 1,子类MyArray 
    function MyArray(color, ...args) {
        var instance
        // 2,借用Wrapper修饰子类this对象返回result
        var result = Wrapper.call(this, ...args);
        // 9,根据Wrapper函数返回值类型result判定子类最终返回的实例对象是result还是子类this对象
        instance = result instanceof Object ? result : this
        // 10,最终返回实例对象挂载其他属性
        instance.color = color;
        // 11,返回实例对象
        return instance;
    }
    // 12,MyArray原型对象的原型设置为Wrapper的原型对象(即MyArray继承Wrapper的原型方法)
    MyArray.prototype = Object.create(Wrapper.prototype, { constructor: { value: MyArray, writable: true, configurable: true } });
    // 13,MyArray原型为Wrapper(即MyArray继承Wrapper上静态方法与属性)
    Object.setPrototypeOf(MyArray, Wrapper);
    
    

17,class继承详解

我把这部分放在《ES6的继承使用ES5完全实现》后面是因为,当看完了《ES6的继承使用ES5完全实现》在看这部分就不费力了。

  • 1,子类继承父类,如果显式创建了构造器函数constructor,那么必须使用调用super,且位于构造器函数顶部。因为super方法在当函数使用时代表父构造函数,而在ES6继承中是将子类的this交给父类构建出未来的实例对象,然后子类继续在该未来的实例对象上继续修饰,所以如果没有super调用,子类将获取不到未来将要输出的实例对象。

  • 2,子类中,只有调用super之后才能使用this,原因同1,子类中的this相当于上面 未来的实例对象。

  • 3,子类继承父类,如果没有显示创建构造器函数,那么该方法会被默认添加。

     class Cat extends Animal {
        // 如果没有显式书写constructor,将默认添加该constructor
        constructor(...args) {
            // super必须位于构造器顶部
            super(...args)
            // other code ...
        }
    }
    
  • 4,Object.getPrototypeOf(a):获取a的原型(__proto__),即返回a.\_\_proto__

  • 5,Object.setPrototypeOf(a,b):将a的原型设置为b,即a.__proto__ = b

  • 6,super关键字可以作为函数使用也可以作为对象使用,作为函数使用只能用与构造器方法(constructor)中。注意 使用super必须明确super用于函数还是对象,如下:

    class Cat extends Animal {
        constructor(...args) {
            super(...args)
            console.log(super.run)
            console.log(super['run'])
            // 上面三种super使用均合法
            // 下面这种super使用不合法,不能明确super是函数还是对象
            console.log(super);
        }
    }
    
  • 7,super作为对象使用时在静态方法中super指向父类,在普通方法中super指向父类原型。如下:

    class Animal {
        run() { console.log('run'); }
    }
    
    class Cat extends Animal {
        // 输出run函数,因为super在普通方法(原型上的方法)中使用,指向父类原型对象
        showBehavior() { console.log(super.run); }
        // 输出undefined,因为super在静态方法中使用,指向父类,父类没有挂载run属性
        static showBehavior() { console.log(super.run); }
    }
    
  • 8,类同时拥有原型与原型对象,即Cat.__proto__Cat.prototype都有,如果Cat继承Animal则Cat.__proto__ === AnimalCat.prototype.__proto__=== Animal.prototype,分别表示子类继承父类构造函数(这样通过原型链子类能够继承到父类上的静态方法与属性)与子类继承父类原型方法(这样通过原型链子类可以继承到父类上原型方法),这两个行为分别对应《ES6的继承使用ES5完全实现》下面两行代码。

    // 9,子类继承父类上的静态属性与静态方法
    Object.setPrototypeOf(Cat, Animal)
    // 10,和寄生组合继承中子类继承父类原型对象一模一样
    Cat.prototype = Object.create(Animal.prototype, { constructor: { value: Cat, writable: true, configurable: true } });
    

    同时如果当前类没有显式继承任何父类,以Animal为例,因为Animal作为一个基类(不存在任何继承),就是一个普通函数,所以Animal.__proto__ === Function.prototypeAnimal.prototype.__proto__ === Object.prototype

18,什么是for...in

  • for...in 以任意顺序遍历一个对象除了Symbol属性之外的可枚举属性
  • for...in是为了遍历对象属性而构建的,不建议与数组使用
  • for...in也会获取当前对象原型链上的可枚举属性,所以通常我们在for...in加入当前对象.hasOwnProperty(key)的方式获取当前对象的属性

19,Object.assign它是一个浅拷贝还是深拷贝

浅拷贝

20,什么是防抖,什么是节流,如何实现

  • 防抖:高频事件连续触发,如果两个连续触发的事件间隔在n秒内,只执行一次事件。(比如当一些搜索框会有联想词出现,并不是每输一次就调一次联想词查询接口,而是当你输完一段时间后如果没继续输才调)
  • 节流:n秒中高频事件连续触发,只执行一次,稀释函数执行频率(比如有些函数不希望被连续执行,类似抢购商品时,你不停点击抢购按钮,这时候使用节流可以是你连续点击10次可能实际只起作用的抢购只有两次)
  • 二者区别:防抖是连续执行的函数最后只执行一次,而节流是连续执行的函数每段时间执行一次。
  • 实现防抖
      // fn非立即执行
        function debounce(fn, timeout = 1000) {
            let timer = null
            return function (...args) {
                clearTimeout(timer)
                timer = setTimeout(() => fn.call(this, ...args), timeout)
            }
        }
        // fn立即执行
        function debounce1(fn, timeout = 1000) {
            let timer = null
            let flag = false
            return function (...args) {
                clearTimeout(timer)
                !flag && (flag = true) && fn.call(this, ...args)
                timer = setTimeout(() => flag = false, timeout);
            }
        }
    
  • 实现节流:
        // fn非立即执行
        function throttle(fn, timeout = 1000) {
            let timer = null
            return function (...args) {
                if (!timer) {
                    timer = setTimeout(() => {
                        fn.call(this, ...args)
                        timer = null
                    }, timeout);
                }
            }
        }
        // fn立即执行
        function throttle1(fn, timeout = 1000) {
            let timer = null
            return function (...args) {
                if (!timer) {
                    fn.call(this, ...args)
                    timer = setTimeout(() => {
                        timer = null
                    }, timeout);
                }
            }
        }
    

21,如何实现一个请求超时

使用Promise.race

 function request_(requestFn, timeout) {
            return Promise.race([
                requestFn(),
                new Promise((resolve, reject) => {
                    setTimeout(() => {
                        reject('请求超时')
                    }, timeout);
                })
            ])
        }

22,Ajax的原生写法

  • XMLHttpRequest:XMLHttpRequest为向服务器发送请求和解析服务器响应提供了流畅接口,也能以异步方式从服务器获取更多信息。简而言之:XMLHttpRequest为浏览器提供了与服务器交互的能力。使用new XHLHttpRequest 创建xhr对象与服务器进行交互。
  • xhr的几个方法属性含义
    • xhr.open(method,url,isAsync):xhr.open 即准备一个请求以备发送,但此时还未发送真正请求,三个参数即请求方法,请求地址,是否异步(默认true,即异步),注意:同步的xhr已经被弃用,原因是它会对最终用户产生不利影响,所以我们这里只说明异步情况下的xhr。
    • xhr.send(data):向服务器发送请求,参数data即请求体数据,一般get方法没有请求体,参数包含在url中,data需要传入null,因为这个参数对于有些浏览器是必须的。
    • xhr.onreadystatechange:当发送异步请求时,只有当xhr.readyState为4时,来自服务端数据才完全接受,而每当readState发生变化时,onreadystatechange方法都会被调用,所以我们使用该方法监听readySate,判断来自服务端数据完全接受,同时配合状态码检测(即2xx/304),判定当前数据响应成功。
    • xhr.readyState几个不同状态含义。
      • 0:尚未调用xhr.open方法,即请求还未准备好
      • 1:已经调用xhr.open方法,但xhr.send方法还未调用,即请求准备好,还未发出
      • 2:已经调用xhr.send方法,但尚未接收到响应,即请求已发出,但是未接收到服务端响应
      • 3,接收到部分响应数据
      • 4,响应数据全部接收,可以在客户端使用
  • 利用Promise实现ajax
     const ajax = function ({ method = 'get', url, data = null }) {
        // 1,请求方法转小写 方便判断
        method = method.toLowerCase()
        // 2,创建xhr对象进行服务器交互
        const xhr = window.XMLHttpRequest ? new XMLHttpRequest : new ActiveXObject
        return new Promise(resolve => {
            // 3,监听xhr的readyState,当接收完成服务端数据,更新Promise状态,保存数据
            xhr.onreadystatechange = function () {
                 if (xhr.readyState === 4) {
                    if (xhr.status === 304 || (xhr.status >= 200 && xhr.status < 300)) {
                        resolve(xhr.response)
                    } else {
                        reject(xhr)
                    }
                }
            }
            // 4,准备一个请求,等待发送
            xhr.open(method, url)
            // 5,如果是post请求,需要设置请求体数据类型为表单类型 'application/x-www-form-urlencoded'
            // 否则一般默认请求体数据类型为 'text/plain',即纯文本,此时服务端可能不会将请求体数据解析成表单类型
            method === 'post' && xhr.setRequestHeader('content-Type', 'application/x-www-form-urlencoded')
            // 6,向服务器发送请求
            xhr.send(data)
        })
    }
    //  ajax({ method: 'post', url: 'http://127.0.0.1:3001/post', data: 'a=1&b=2' }).then(text => console.log('text:', text))
    
    • 注意:要成功发送请求头信息,需要在xhr.open之后,xhr.send之前进行设置。

23,Promise.all

  • 作用: all接收一个Promise数组list,执行list中所有Promise.then,获取每个Promise成功值,并将其储存在数组res中,所有Promise.then执行完,返回值为res的Promise,如果list中任意Promise状态变为失败,则立即返回失败状态Promise,值为首先失败的Promise的值。
            Promise.all_ = function (list) {
                return new Promise((resolve, reject) => {
                    let res = [], counter = 0, len = list.length;
                    list.forEach((p, i) => {
                        p.then(e => {
                            res[i] = e;
                            ++counter === len && resolve(res)
                        }).catch(err => {
                            reject(err)
                        })
                    })
                })
            }
    
    • 代码中counter与len作用: 因为Promise.all返回的是个Promise实例,其值为数组,数组key对应值为参数key对应的Promise实例成功值, 所以我们需要借助len与counter维持key与值的对应关系, 如果只使用数组长度,没有count辅助,那么可能参数最后的Promise实例先完成,导致直接返回[empty*n,value]

24,Promise.prototype.catch

作用:捕获Promise失败状态的原因,Promise内部任何异常都会被捕获且一旦出现异常,Promise状态立刻变成失败,保存异常值。

  Promise.prototype.catch_ = function(fn){
          return  this.then(null,fn)
        }

25,Promise.race

作用:接收一个Promise数组,率先返回状态的将状态同步到一个Promise中,且返回该Promise。

  Promise.race = function (list) {
            return new Promise((resolve, reject) => {
                list.forEach(e => {
                    e.then(e => resolve(e)).catch(err => reject(err))
                });
            })
        }

26,Promise.prototype.finally

  • finally作用:接上finally的promise实例,无论promise实例状态成功还是失败都会执行finally中的回调,finally后如果继续接then,那么then处理的是finally之前的promise实例。一般需要进行兜底可以使用finally处理。
Promise.prototype.finally = function (callback) {
    // 1,使用Promise.resolve 包裹callback,保证callback出现异步任务也能按顺序执行,
    // 2,当然这么做还会产生一些其他副作用,比如callback中的错误会优先于finally之前promise实例捕获等,有兴趣的可以自行研究。
    return this.then(
        e => {
            return Promise.resolve(callback()).then(() => e)
        },
        err => {
            return Promise.resolve(callback()).then(() => { throw err })
        }
    )
}

关于注释1,以下面这个例子,需要确保执行顺序是123456finished,所以使用return Promise.resolve(callback()).then(()=>e)而不是callback();return Promise.resolve().then(()=>e)

Promise.resolve('finished.').finally(function () {
    return Promise.resolve().then(() => console.log('1')).then(() => console.log('2')).then(() => console.log('3'))
        .then(() => console.log('4')).then(() => console.log('5')).then(() => console.log('6'))
}).then(v => {
    console.log(v);
})

27, Promise.retry

作用:实现Promise失败给定次数重试,还不成功则失败(Promise上无该方法)

Promise.retry=(fn, num)=> {
    return new Promise((resolve, reject) => {
        function run() {
            fn().then(resolve)
                .catch(err => num-- === 1 ? reject(err) : run())
        }
        run()
    })
}

27, Promise.allSettled

Promise.allSettled_ = function (list) {
    return new Promise((resolve, reject) => {
        let res = [], count = 0;
        list.forEach((p, i) => {
            p.then(v => {
                count++; res[i] = { status: 'fulfilled', value: v };
                count >= list.length && resolve(res)
            }).catch(err => {
                count++; res[i] = { status: 'rejected', reason: err };
                count >= list.length && resolve(res)
            })
        })
    })
}
// test:
const p1 = new Promise((r, re) => setTimeout(r, 1000, 1))
const p2 = new Promise((r, re) => setTimeout(re, 1000, 2))
const p3 = new Promise((r, re) => setTimeout(r, 2000, 3))
Promise.allSettled_([p1, p2, p3]).then(v => console.log(v))

28,变量对象初始化顺序

var 声明初始化,形参初始化,函数声明初始化(函数没有块作用域)

29,了解JS的作用域么,函数作用域如何确定以及如何查找。

  • 作用域:作用域函数或者全局环境中变量函数能够进行查找的范围,且JavaScript使用静态词法环境,即静态作用域,对于函数,其作用域在其创建的时候就能确定了。

  • 函数作用域的确定:当创建函数时,函数内部会生成[[scope]]属性,保存了父级作用域链,当执行函数时,首先会创建函数执行上下文,包括变量对象(变量对象包括arguments对象,形参初始化,函数声明初始化,变量声明初始化),函数this指向,以及作用域链,作用域链即将[[scope]]属性与函数变量对象连接在一起。 当函数执行上下文创建完毕时,执行函数代码,此时函数内的变量函数形参等查找,将会顺着作用域链,从当前函数变量对象向上一直查找到全局变量对象。

30,说一说事件循环以及宏任务,微任务。

  • 事件循环:js是单线程的,主线程执行js代码时,遇到异步代码会将其放入事件队列中,继续执行主线程中的代码,当js的监视进程检测到主线程中代码执行完毕,就会按顺序取出之前放入事件队列中的异步代码执行,在此过程中,js监视进程不断的检查主线程执行栈是否为空的,为空就去事件队列中获取事件执行,如此循环。
  • 具体来说,我们的异步任务分为两种,一种是宏任务一种是微任务,宏任务包括整体js代码,定时器,UI交互事件等,微任务常见的就是Promise的then,catch,finally以及node中的process.nextTick,这里我们拿宏任务定时器与微任务Promise.then举例。
    • 当我们的主线程遇到定时器任务时会将其注册到宏任务事件表中,定时任务由浏览器计时,当计时完成,注册在宏任务事件表中的定时器任务将会进入到宏任务事件队列中,
    • 主线程代码继续执行遇到微任务Promise.then时同样的then注册微任务事件表中,Promise状态发生变化,then进入微任务事件队列中。
    • 主线程代码执行完毕时会先去检查微任务队列中是否有待执行的微任务事件,有则取出执行,执行完毕继续检查微任务队列是否有待执行微任务,有则取出继续循环上面操作
    • 无则检查宏任务队列是否有待执行的宏任务,有则取出执行,执行完宏任务,继续检查微任务队列是否有待执行微任务,有责按照上面微任务操作进行,无则检查宏任务队列,如此反复。

31,浏览器的事件机制有哪几个阶段?target与currentTarget区别?如何阻止事件冒泡以及如何阻止事件默认行为。

  • 三个阶段即捕获阶段,目标阶段,以及冒泡阶段。
    • 1,事件捕获阶段,事件由根节点流向目标节点,途径各个节点若其绑定了事件捕获函数,则触发该函数
    • 2,目标阶段:事件到达目标节点,执行绑定在目标节点上的函数,即目标阶段。如果目标节点同时绑定捕获函数与冒泡函数,那么目标阶段先执行目标节点捕获函数,再执行目标节点冒泡函数。
    • 3,事件冒泡阶段:事件由目标节点冒泡至根节点,途径各个节点若其绑定了事件冒泡函数,则触发该函数。聚焦失焦等无冒泡。
    • ps:dom0事件(类似document.onclick)在事件冒泡阶段触发
    • ps:在dom2事件中,如果同一节点绑定多个冒泡或者捕获函数(dom0看成冒泡函数),那么会按照绑定顺序依次执行
    • ps:dom2事件(document.addEventListener('click',fn,false))第三个参数默认为false,即绑定事件冒泡函数,true为事件捕获函数。
  • target与currentTarget区别:如图当我实际点击span区域,事件阶段是div捕获,span捕获,span冒泡,div冒泡。 image.png
    • target:即真正发生事件的dom元素,这里点击span,所以target时span元素
    • currentTarget:即在事件捕获冒泡阶段,当前事件捕获或者冒泡到哪一个元素,那么该元素即currentTarget,比如点击span区域,div捕获函数命中那么其target是span元素,currentTarget是div元素,当span捕获函数命中,target是span元素,target是span元素。
  • 阻止事件冒泡与阻止默认行为
     document.addEventListener('click', function (e) {
        // ie使用window.event.cancelBubble=true阻止冒泡
        // 其他浏览器使用e.stopPropagation()阻止冒泡,e.stopPropagation()还可以阻止事件捕获
        window.event ? window.event.cancelBubble = true : e.stopPropagation()
    
        // ie使用window.event.returnValue = false阻止默认行为
        // 其他浏览器使用e.preventDefault()阻止默认行为
        window.event ? window.event.returnValue = false : e.preventDefault()
    })
    

32,数组原型上面的方法有哪些(使用说明忘了看MDN🐶)

// concat entries every fill filter
// find findIndex flat flatMap forEach
// includes indexOf join keys lastIndexOf
// map push unshift pop shift 
// reduce reduceRight reverse slice some
// sort splice values

33,typeof null 为什么是 'object'?

第一版js 使用32bite存储值,判断数据类型由32位中低三位判断,由于对象与null的低三位相同,typeof源码未对null进行过滤,导致判断低三位出现 null与对象类型相同。

34,forEach如何跳出循环

使用trycatch包裹froEach,当需要跳出时抛出一个错误,像下面这样。

console.log('start');
try {
    [1, 2, 3].forEach(e => {
        console.log('e:', e);
        if (e === 2) throw new Error('jump')
    })
} catch (error) {
    // console.log('error,', error);
}
console.log('end');

35,if(a==1&&a==2&&a==3) 成立。

  • 改写对象valueOf方法,初始值0,每次返回值+1,这种方法只能对于非精确等于(==)生效,而对于精确等于(===)不生效。
        const a = {
            _a: 0,
            valueOf() {
                return ++this._a
            }
        }
        if(a==1&&a==2&a==3){
            console.log('win !!!');
        }
    
  • 劫持访问器属性get,初始值0,每次访问get返回值+1,这种方法对于精确等于非精确等于都生效。
        let _a = 0
        Object.defineProperty(window, 'a', {
            get() {
                return ++_defa
            }
        })
        if (a == 1 && a == 2 & a == 3) {
            console.log('win !!!');
        }
    

36,instance of 原理

instance of简称inf,inf判断依据是左边的__proto__是否等于右边的的prototype,等于返回true,不等于继续顺着原型链继续对比prototype.__proto__,一直对比下去如果找不到相等返回false。

37,null和undefined的区别

  • null:标识没有对象,此处不该有值,且Number(null)为 0
  • undefined:此处应该有值,但还没定义,且Number(undefined)为NaN

39,common.js 与 es6模块化区别

  • common.js运行时输出,es6模块化编译时输出
  • common.js输出的值拷贝,es6是对值的引用
  • common.js this指向当前模块,es6为undefined

40,函数组合(compose)

  • 函数组合:如果多个函数fn1,fn2,fn3,每个函数返回参数是下个函数参数,那么我们想获得最终结果可能需要这么做fn1(fn2(fn3(x))),而compose函数就帮我们做了这件事。多余任意多个符合上述条件函数进行组合,
  • compose实现
      function do1(action) {
            return action = action + '_do1'
        }
        function do2(action) {
            return action = action + '_do2'
        }
        function do3(action) {
            return action = action + '_do3'
        }
        // compose 实现 
        function compose(...fns) {
            return fns.reduce((pre, next) => {
                return function (x) {
                    return pre(next(x))
                }
            })
        }
        // do1(do2(do3('action'))) === compose(do1, do2, do3)('action')
    

41,垃圾回收

  • 什么是垃圾回收:垃圾回收即当内存不再需要使用时释放内存。

  • 如何判断当前内存是否可以回收:在垃圾回收时,我们首先得知道哪些内存属于需要释放的内存,即如何判定当前内存是不需要使用的,而引用计数算法标记清除算法就是用来做这件事的。但是其中引用计数算法会有循环引用的弊端,2012年后,所有现代浏览器都使用标记清除算法进行垃圾收集。

    • 引用计数算法:引用计数算法判定垃圾的方法是:把对象是否需要简化成对象有没有被引用,即当前对象是否存在被引用,存在就不是垃圾,不存在就是垃圾。 所以引用计数垃圾清除的过程就是:当前引用类型如果不被任何人引用(被引用次数为0),那么该引用类型占用内存就可以被清除,但是这样有个弊端,出现循环引用将无法被清除。

      //  {aa:1}被a引用,此时 {aa:1}不会被清除
      let a = {aa:1}
      //  a =null,a对 {aa:1}引用解除,无人引用 {aa:1}, {aa:1}将会被清除
      a = null 
      
      var a = {aa: c}
      var c = {cc: a}
      a = null
      c = null
      // 虽然 a,c解除对{aa: c}与{cc: a}的引用,但是{cc: a}与{aa: c}互相引用对方,所以二者被引用次数永远为1,他们就不会被清除
      
    • 标记清除:标记清除算法判定垃圾的方法是:把对象是否需要简化成对象是否可以获得或者说对象是否可以到达,即垃圾回收器定期从根节点集合(主要指的是全局对象与执行栈)开始,垃圾回收器会跟踪每一个指向javascript对象的指针,并将这些对象标记为可访问的,同时跟踪这些对象中每个指向javascript对象的指针,继续标记可访问的,这个过程递归进行,直到标记到运行时的每一个可访问的对象。对那些不可访问的对象进行清除。 标记清除可以避免循环引用的问题。

      • 全局对象查找过程:我们从全局对象下找到所有声明的对象,那么这些对象就是可达对象,继续顺着这些可达对象内部的对象指针找到下一层的可达对象,依次递归直到所有对象查找结束。
      • 执行栈查找过程:函数执行前会创建执行上下文,执行上下文包括了变量对象,作用域链,即从执行时的执行栈中去递归寻找这些执行上下文中的可达对象并标记。
  • V8垃圾回收策略: V8垃圾回收策略基于分代式垃圾回收机制,主要将堆内存空间分为新生代与老生代两个区域,且新生代与老生代采用不同的垃圾回收算法(其实除了将堆内存分为新生代与老生代,还有其他区域,如下图,但对于其他区域没必要太多深入)

    image.png

  • 新生代: 新生代主要存放存活时间较短对象,使用Scavenge(中译:打扫,清扫)算法进行垃圾回收,具体过程如下:

    • 1,新生代由两个相等大小的内存空间构成,一个叫from空间,一个叫to空间,新分配的对象会进入from空间,此时to空间是空的。

    • 2,当进行垃圾回收时(Scavenge算法运行),会将标记(标记阶段作用于整个堆)的活动对象复制到to空间连续内存中(顺带整理了内存,当然复制后的对象指针也要更新,因为内存中位置变了,所以每一个复制对象会留下一个转发地址指向新地址,用于指针更新到新地址),并清除from空间中的非活动对象(就是垃圾了)。

    • 3,然后将from空间与to空间互换,此时from空间就剩下所有活动对象,to空间是空的,下一次副垃圾回收器清理时继续执行该操作。

  • 对象晋升(活动对象从新生代晋升至老生代)的两种方式:

    • 1,当前对象是否经历过一次Scavenge算法: 默认情况下,我们创建的对象都放在新生代的from空间内,当进行垃圾回收时,将对象从from空间复制到to空间之前,会检查该对象内存地址判断其是否经历过一次Scavenge算法,如果该对象地址已经发生变动,那么该对象将进入老生代中,不再进入新生代的to空间内。这个过程很好理解,如下图:

      image.png

    • 2,to空间内存占比是否已经超过25%: 即当垃圾回收时,将对象从from空间复制到to空间之前,如果活动对象没有经历过Scavenge算法,将被复制到to空间,但是,如果此时to空间内存占比已经超过25%,那么该活动对象将会被转移到老生代,不再进入新生代的to空间内。该过程如下图:

      image.png

    • tips:关于25%的内存限制: 该限制主要因为在经历过一次Scavenge算法之后,from空间与to空间会互换,如果互换后的from空间内存使用率过高,会影响后续内存的分配(因为新入的对象都分配在当前新生代from空间内),因此设定25%的限制,防止角色互换后的from空间占用过高或溢出。

老生代: 老生代中因为管理着大量存活对象,如果继续使用Scavenge算法会浪费一半内存。因此在老生代中,我们使用新的算法标记清除(Mark-Sweep)标记整理(Mark-Compact) 进行内存管理(而在以前这里使用的是引用计数算法,前面有说,因为引用计数弊端,2012年后主流浏览器放弃了该算法)。

  • 标记-清除算法:

    • 标记: 从根节点集合(全局对象,执行栈)开始遍历,找到可达对象,标记为为活跃对象,那些不可达对象就是非活跃对象。

    • 清除: 一旦标记完成,垃圾回收器将找到所有非活跃对象内存空间,并将其内存空间地址记录在空闲列表中(空闲列表中内存块记录以内存块大小区分,方便日后整理阶段中根据活跃对象内存大小来复制活跃对象)。

  • 标记-整理算法:

    • 标记: 从根节点集合(全局对象,执行栈)开始遍历,找到可达对象,标记为为活跃对象,那些不可达对象就是非活跃对象。

    • 整理: 在清除的基础上,对高度分散的内存页中的活跃对象复制到未被整理的其他内存页中(即复制到 先前清除阶段 空闲列表页记录的 非活跃对象地址)。

    • tips:因为整理阶段中复制活跃对象会带来很高成本,所以整理阶段只针对于高度分散的内存页进行整理,对于其他内存页仅进行清除操作。

  • V8垃圾回收优化:因为JS是单线程的,所以在垃圾回收器运行的时候会出现全停顿现象(stop-the-world),即暂停应用程序,执行垃圾回收,所以为了减少垃圾回收带来的停顿时间,出现了并行垃圾回收,增量垃圾回收以及并发垃圾回收技术。

    • 并行垃圾回收:利协助线程配合主线程一起处理垃圾回收工作,这样主线程上的垃圾回收时间就会缩短,虽然还是全停顿的垃圾回收方式,但是大大缩短了全停顿的时间。因为有多个线程一起处理垃圾回收肯定比主线程一个处理块。 image.png
    • 增量垃圾回收:即主线程间歇性的做一部分垃圾回收工作,多个间歇性的过程完成整个垃圾回收工作,虽然本质上没有减少主线程暂停去处理垃圾回收时间(可能还有增加),但是通过将垃圾回收过程分成一小段一小段间歇性执行,这样就可以使得应用程序间歇性执行从而有时间响应动画或者用户输入等。 image.png
    • 并发垃圾回收:主线程一直执行JS,辅助线程一直在后台执行垃圾回收,这种方式相对于以上两种实现最难,因为js的执行,堆中内存可能一直在发生变化,导致之前垃圾回收做的工作无效,而且还有可能主线程与辅助线程同时操作同一个对象。但是这种方式优点也很明显,不会阻碍主线程执行。 image.png
  • 常见内存泄露场景:

    • 无用的的全局变量
    • 忘记清除的定时器
    • 忘记清除的监听事件
    • 对象的循环引用(标记清除中应该不存在这个问题)
    • 闭包的不合理使用:比如将一个无用的携带大量作用域的闭包函数暴露到全局中。

42,实现函数柯理化

  • 柯理化原理:柯理化是将多参数转换成使用一个参数的一系列函数的过程,当参数接收满足原函数的参数长度,那么将执行原函数并传入之前接受的一系列参数。
  • 柯理化用途
    • 参数复用:比如我们有一个函数传入两个参数,第一个参数为正则,第二个参数为正则校验的电话号码,如果我们需要用同一正则校验多个号码,那么我们可以将该函数柯理化,对正则参数进行缓存,之后每个电话号码校验只需要使用缓存正则后的柯理化函数传入不同电话号码即可,不需要每次都传入正则与电话号码两个参数。
    • 延迟函数执行
  • curry函数实现
     function curry(fn, ...args1) {
        return function x(...args2) {
            args1 = [...args1, ...args2]
            if (args1.length < fn.length) return x
            return fn(...args1)
        }
     }
    // const add = (a, b, c, d) => a + b + c + d
    // const add_ = curry(add) // curry转换
    // console.log(add_(1)(2)(3)(4) === add(1,2,3, 4));
    

43,手写发布订阅与观察者模式,以及他们区别

  • 观察者模式:即分为观察者与目标,类似于发布订阅中的订阅者与发布者,观察者实现自己的功能函数,同时观察者注册到目标中,目标在某一时间通知所有注册到当前目标内的观察者执行自己的功能函数
        // 目标 
        class Subject {
            constructor() {
                // 注册进目标中的观察者存放位置
                this.observerList = []
            }
            // 目标的注册观察者函数
            push(...observers) {
                this.observerList.push(...observers)
            }
            // 目标通知所有注册观察者执行自己的功能函数
            notify() {
                this.observerList.forEach(e => e.update())
            }
        }
    
        // 观察者
        class Observer {
            constructor(callBack) {
                // callBack 非函数抛出错误
                if (typeof callBack !== 'function') throw new Error('we need a function !')
                // callback 即自定义观察者的功能函数
                this.callBack = callBack
            }
            // 执行当前观察者功能
            update() {
                this.callBack()
            }
        }
    
        // const subj = new Subject(),
        //     observer1 = new Observer(_ => { console.log('我是1号观察者'); }),
        //     observer2 = new Observer(_ => { console.log('我是2号观察者'); })
    
        // subj.push(observer1, observer2)
        // subj.notify()    
    
  • 发布订阅模式:分为订阅者,发布者(以及调度者),订阅者将订阅函数注册到发布者内部的队列中,某一时刻发布者通过调度者通知当前所属事件类型的订阅函数执行。这里的调度者所做的事,就是根据事件类型执行对应的订阅函数,调度者的存在也是与观察者模式的最大不同。
    class EventEmitter {
        constructor() {
            this._events = {}
        }
        on(type, callback) {
            if (!this._events[type]) this._events[type] = []
            this._events[type].push(callback)
        }
        once(type, callback) {
            if (!this._events[type]) this._events[type] = []
            const helpFn = (...args) => {
                callback(...args)
                this.off(type, helpFn)
            }
            this._events[type].push(helpFn)
        }
        emit(type, ...args) {
            if (!this._events[type]) return
            this._events[type].forEach(fn => fn(...args))
        }
        off(type, callback) {
            if (!this._events[type]) return
            this._events[type] = this._events[type].filter(fn => fn !== callback)
        }
    }
    
  • 二者区别:
    • 1,发布订阅模式中有调度者根据不同类型事件的执行对应的函数,而观察者模式这没有调度者,而是对所有观察者的方法全部执行局。
    • 2,观察者模式耦合度比发布订阅大,观察者模式规定观察者挂载同名功能函数,方便目标执行,这就使得观察者与目标出现了耦合,而发布订阅中的发布者与订阅者不存在这种耦合,如果非要说发布订阅模式中存在耦合,那么只能是发布者与调度者耦合在一起。

44,什么是稀疏数组,造成稀疏数组的操作有哪些

  • 稀疏数组:稀疏数组就是存在空位的数组,我们经常看到[empty × 3]就是稀疏数组。
  • 造成稀疏数组的方式:数组长度大于数组中实际元素的数量将产生稀疏数组。
    • 1,Array(n):n为数字类型
    • 2,length长度大于数组实际长度:如图索引3,4出现空位。 image.png
    • 3,[,,,]:也会出现3位稀疏数组
    • 4,delete删除数组某一项,该项会变成empty:如图删除索引1项,出现空位 image.png
    • 5,指定数组长度大于实际长度 image.png
    • 6,使用数组原型上方法对类数组进行转换成数组,如果对应length的索引在类数组中没有对应属性,那么也会出现稀疏数组。 image.png

44,什么是类数组,类数组转换成数组方法

  • 类数组:具有length属性的对象就是类数组。
  • 类数组转换成数组方法:
    • 1,使用ES6中扩展运算符...:扩展运算符适用于存在迭代器对象属性的类数组,比如函数中的arguments对象存在迭代器对象属性,即arguments[Symbol.iterator]存在,那么就可以实现使用扩展运算符let args = [...arguments],当然我们也可以自己实现迭代器对象。对于存在迭代器属性的类数组推荐使用
    • 2,使用Array.from:Array.from(任何类数组)即可返回转换后数组,而且不会输出稀疏数组,注意from方法是挂载数组构造函数上,而不是数组原型对象上。对于任何类数组推荐使用
    • 3,使用数组原型上的一些方法:比如Array.prototype.slice.call(arrayLike),slice方法有弊端,即对于类数组length对应的索引在类数组中没有对应属性,那么会输出稀疏数组,并不是很推荐使用。 不过我们可以使用slice对数组实现数组浅拷贝arr.slice()
    • 4,自己实现类数组转数组方法:不会出现输出稀疏数组的情况。不嫌麻烦就用吧
          function fn(arrayLike) {
              const res = []
              for (let i = 0; i < arrayLike.length; i++) {
                  res[i] = arrayLike[i]
              }
              return res
          }
      

45,下面两个for循环输出差异问题在哪

    const arr = [0, 1, 2]
    for (let i = 0; i < arr.length;) {
        setTimeout(() => console.log(i), 0);
        i++
    }
    for (let i = 0; i < arr.length; i++) {
        setTimeout(() => console.log(i), 0);
    }
  • let 创建块作用域,第一个循环内部由于使用了定时器任务,i++与定时器任务操作的是同一个块作用域内的i,所以i++改变i值,定时器i值跟着变化,所以导致输出1,2,3,第二个循环由于i++放在了for内,i++属于下一个块作用域 (当前块作用域的i=上一个块作用域的i+1),所以i++不会影响到上一个定时器任务内i值,所以输出0,1,2。

46,什么是遍历器(Iterator)

  • 遍历器:Iterator为各种数据结构提供一个统一简便的遍历接口,遍历器本质是一个函数,返回一个指针对象,每调用一次指针对象提供的next方法,指针对象指针都会指向数据结构下一个成员,next方法返回一个对象{done:false,value:1},done代表是否遍历结束,value即当前数据结构成员。而遍历器接口主要提供给for...of...消费,即for...of...遍历时,会自动寻找遍历器接口。所以提供了遍历器的数据结构都可以for...of...遍历。除了for...of...,扩展运算符...与解构赋值let [a,b] = [1,2];也会默认寻找当前数据结构的遍历器接口。
  • 默认具有遍历器的数据结构:数组,Map,Set,字符串,arguments对象,dom节点对象等
  • Iterator部署位置:遍历器默认部署在数据解构的[Symbol.iterator]属性中,当然我们自己定义数据结构期望可以遍历,我们可以自己部署该属性,部署在当前数据结构上const object = {};object[Symbol.iterator] = 遍历器函数,或者部署在其原型上const object = {};Object.prototype[Symbol.iterator] = 遍历器函数
  • 实现自定义类数组的遍历器:我们只需要在类数组上实现遍历器即可。
    const object = {
        '0': 0,
        '1': 1,
        '2': 2,
        length: 3
    }
    Object.defineProperty(object, Symbol.iterator, {
        value: function () {
            let index = 0
            const len = this.length
            return {
                next: () => {
                    return index < len ? { done: false, value: this[index++] } : { done: true, value: undefined }
                }
            }
        }
    // 或者借用数组实现好的遍历器,改变this指向当前object
    //  value: function () {
    //        return [][Symbol.iterator].call(this)
    //    }
    })
    console.log([...object]); // [0,1,2]

47,generator函数

  • 什么是generator函数:
    • 1,从表现形式上,generator函数可以理解为一个状态机,每个yield后面为不同的状态。
    • 2,从函数执行上看,generator是个遍历器生成函数,执行generator将会返回一个遍历器对象,遍历器对象可以依次遍历generator内部每一个状态。
    • 3,从工作原理上,generator是协程的一种实现,当执行到generator函数,其执行上下文会加入执行栈,当遇到yield表达式,generator执行上下文会暂时退出执行栈,但不会消失,里面的所有变量与对象状态将冻结,等到执行.next命令时,generator函数恢复执行,其执行上下文又加入执行栈,冻结的变量与对象恢复执行。
      • 3.1,什么是协程:协程是一种程序运行方式,可以使用单线程实现,也可以使用多线程实现,多线程中协程属于一种特殊的线程。协程中,多个线程(如果是单线程则是多个函数)可以并行执行,但是只能有一个线程(函数)处于运行状态,其他线程(函数)处于暂停态,线程(函数)之间可以交换执行权,也就是说一个线程(函数)执行到一半可以将执行权交给另一个线程(函数),等到稍后收回执行权的时候再恢复执行,这种可以并行执行,交换执行权的线程(函数),即协程。
      • 3.2,协程与普通线程差异:普通线程同一时间可以多个线程同时运行,但对于协程,只能有一个线程处于运行态,其他协程处于暂停态,除此之外普通线程资源是抢占式,由其运行环境决定,协程属于合作式,由自己分配。
      • 3.3,js中的generator的协程实现:由于JS是单线程,所以JS中协程以单线程实现,即协程只是一个函数,交出收回协程函数的执行权,以generator函数来说,通过yield交出其执行权,与此同时,其执行上下文将会冻结并退出执行栈,但不会消失,此时generator函数处于暂停态,当再次调用next命令,generator函数收回执行权,与此同时,其执行上下文重新加入执行栈,并解冻,继续执行generator函数。

48,async函数

  • async函数是什么:简单的来说,async函数就是generator函数的语法糖(将generator 中的yield与async函数中的await替换,你会发现二者函数内部没啥区别),进一步说,async函数可以看作多个异步操作被包装成一个Pormise对象,其中await就是Promise中then的语法糖。 相对于generator函数,async有以下四点改进。
    • 1,generator需要执行器辅助执行,比如co模块,而async函数内置执行器,不需要其他模块辅助就可以像正常函数一样执行。
    • 2,generator的执行器co模块约定yield后面只能是Promise对象或者Thunk函数,而async函数中,await后面可以是Promise对象,也可以是基本值。
    • 3,generator函数返回的是个遍历器对象,而async返回的是个Promise对象
    • 4,async有更好的语义,async表示函数中存在异步函数,await表示紧跟其后表达式需要等待
  • async函数实现原理:async函数其实就是将generator函数与自动执行器包装在一个函数中,该函数可以自动执行generator函数。下图是具体实现过程,其中async函数的执行过程相当于generator函数配合自动执行器执行。
    // async函数
    async function async_() {
        await Promise.reject(2)
        await 1
    }
    
    // generator函数
    function* generator_() {
        yield Promise.reject(2)
        yield 1
    }
    // generator自动执行器
    function spawn(generator) {
        const gen = generator()
        return new Promise((resolve, reject) => {
            function step(nextF) {
                // 1,let 为块级作用域,直接写在try中会导致之后获取不到next,所以提出来
                let next
                // 2,trycatch 意 在执行权返回generator函数的时候,如果再generator交出执行权之前报错,及时捕获,终止函数,并直接返回失败状态Promise
                try {
                    next = nextF()
                } catch (error) {
                    return reject(error)
                }
                // 3,如果generator执行结束,返回成功状态Promise,值为遍历器对象指向的终值
                if (next.done) return resolve(next.value)
                // 4,使用Promise.resolve包裹 yield后面的表达式/值,如果是Promise直接返回Promise,基本值则返回成功状态Promise,值为该基本值
                // x,这里的Promise.resovle相当于await语法糖,不同版本node中await处理不同,有的是当前Promise.resolve(x),有的是new Promise(r=>r(x))
                Promise.resolve(next.value)
                    .then(e => step(_ => gen.next(e)))
                    // 5,catch作用即Promise返回失败状态,使用gen.throw(err)在下次递归抛出错误,由上面第2步trycatch捕获错误,返回最终失败状态Promise
                    .catch(err => step(_ => gen.throw(err)))
            }
            // js是传值调用,即如果直接step(gen.next()),gen.next()会立即被调用,那么如果出现错误也没办法捕获,所以在gen.next()外层包裹函数,等用到的时候使用tracatch包裹并进行调用,避免了gen.next()出错不能捕获。
            step(_ => gen.next())
        })
    }
    
    // async函数 = generator函数 + generator函数执行器
    // async_()  ===  spawn(generator_)
    
    • 关于await:从上面代码spawn自动执行器不难看出,await x 其实就相当于Promise.resolve(x),如下图代码fn与fn_的作用是相同的,但是对于不同版本V8中的await实现可能不同,比如node13中await x采用Promise.resolve(x)的实现方式,而node11中await使用是new Promise(r=>r(x))实现,关于Promise.resolve(x)与new Promise(r=>r(x))区别,其实最主要的就是在x是Promise对象时,new Promise(r=>r(x))会比Promise.resolve(x)多出两个异步时序。详细区别可以看我另一篇文章。《Promise.resolve(x)与new Promise(r=>r(x))》
          async function fn() {
              await console.log('1')
              console.log('2');
              console.log('3');
              // do others
          }
          function fn_() {
              Promise.resolve(console.log('1'))
                  .then(e => {
                      console.log('2');
                      console.log('3');
                      // do others
                  })
          }
      

49, typeof x === 'undefined' 与 x === undefined 区别

  • 如果x未声明就进行判断, typeof x === 'undefined' 不会报错,x === undefined会报错,事实上,除了在let/const之前使用typeof处理其声明的变量/常量会报错(let,const存在暂存性死区),其他任何变量,不管有无声明使用typeof都不会报错。

  • undefined 他是全局对象中是全局对象的一个属性,即window.undefined === undefined,但是无法赋值全局对象undefined属性,因为其数据属性全为false,不可写,不可配置,不可枚举。我们可以在函数中声明undefined同名变量并对其进行赋值,再这样在函数内使用的undefined将是我们声明的undefined。

50,{},new Object(),Object.create() 区别

  • {}:即对象字面量,原型指向Object原型对象
  • new Object():不传入任何值或者传入null/undefined的时候与{}相同,传入非null/undefined的基础类型会返回基础类型的包装类型,传入引用类型直接返回当前引用类型
  • Object.create():必须得传入第一个参数,且必须是null或者引用类型,该方法将创建一个原型指向第一个参数的对象,传入null则该对象无原型,该方法第二个参数可选,即类似Object.defineProperty第二个参数,指定该对象的具体属性(访问器属性/数据属性)

51,class 与 ES5构造函数 区别

  • class必须先声明再使用,不存在变量提升,ES5构造函数存在变量提升
  • class不允许重复声明,ES5构造函数允许重复声明
  • class只可以使用new调用,ES5构造函数除了new还可以直接执行。
  • class内部声明的静态方法原型方法不可以被枚举,ES5构造函数声明的静态方法与原型方法不做特殊处理时可以被枚举,class静态方法与原型在数据属性中的枚举属性是false,配置属性为true,写属性为true。(可枚举:Object.key(prototype)获取到该属性, 'x' in protype 为true)
  • class内部为严格模式,构造函数可选。
  • class继承与构造函数继承(组合寄生继承)也有区别:
    • class继承中:子类没有自己的this对象,父类创建this对象,先对this进行加工,然后把this交给子类继续加工(所以继承时需要调用super完成,如果不调用super,子类是获取不到this对象,然后报错),所以子类是没有自己的this对象
    • 构造函数中,子构造函数创建this对象,使用Parent.call(this)等方法完成父元素对this的加工,加上子构造函数本身对this的加工,完成this处理,所以子构造函数中有自己的this对象。

52,严格模式

  • 严格模式的意义:消除js代码中不合理不严谨不安全的一些行为,提高编译器效率,增加运行速度,为新版js做铺垫

  • 如何开启严格模式:在脚本或者函数头部添加 "use strict"

  • 严格模式与正常模式区别:

    • 1,严格模式中,属性的数据属性中configurable/writable如果是false,强行删除/重新赋值该属性会报错

      "use strict"
       const obj = {}
       Object.defineProperty(obj, 'x', {
          configurable: false,
          enumerable: false,
          writable: false,
          value: 1
       })
       delete obj.x    // 报错
       obj.x = 1       // 报错
      
    • 2,严格模式中,变量必须声明,否则报错

      "use strict"
       b = 1 // 报错
      
    • 3,严格模式中,形参不可重名,否则报错

       "use strict"
       function x(a, a) { } // 形参重名,报错
      
    • 4,严格模式中不允许在全局环境中 使用delete xxx,不管xxx是否存在是否可配置,否则报错。

      "use strict"
      Object.defineProperty(window, 'xxx', {
          configurable: true
      })
      delete xxx // 报错
      
    • 5,严格模式中,this默认指向undefined而不是全局对象

      "use strict"
      function fn() {
          console.log('this:', this); // undefined
      }
      fn()
      
    • 6,严格模式中,eval与arguments不可以做变量名,否则报错

      "use strict"
      let eval = 1        // 报错
      let arguments = 2   // 报错
      
    • 7,严格模式中,对象属性仅有get方法,该属性如果被赋值,将报错

      "use strict"
      var x = {
          get c() {
              return 1
          }
      }
      x.c = 2 // 报错
      
    • 其他:除此之外,严格模式还有许多其他限制,比如不可是用保留关键字(public等)作为变量名,eval中创建的变量不能被调用等。

53,实现双向绑定

<!DOCTYPE html>
<html lang="en">
<body>
    <input id='input' />
    <script>
        const state = {}
        function twoWayBinding(input, state, key) {
            const symbol = Symbol()
            state[symbol] = input.value = state[key]
            Object.defineProperty(state, key, {
                get() { return state[symbol] },
                set(val) { state[symbol] = input.value = val }
            })
            input.addEventListener('keyup', e => state[key] = e.target.value)
        }
        twoWayBinding(input, state, 'inputVal')
    </script>
</body>
</html>

感谢参考文章: