神秘的JS(持续补充)

681 阅读14分钟

1. 浏览器中的JS执行机制

  • JS并非严格按代码顺序执行,而是JS引擎在“编译阶段”先解析(变量提升)再进入“执行阶段”
  • “编译阶段”会将代码生成两部分,“执行上下文(运行环境)”和“可执行代码(生成字节码)”
  • 执行上下文,实际为一个“变量环境对象”,以对象形式存储着变量提升的内容,又分为“变量环境(var function)”和“词法环境(块级作用域 let const)”
  • var缺陷:作用域是变量/函数定义的位置决定的,决定了变量/函数的可见性和生命周期。之前有全局作用域和函数作用域两种,es6新增块级作用域。导致var缺陷:变量容易在不易察觉的情况下被覆盖掉;本应该销毁的变量没有被销毁;
  • 作用域与let/const:let/const两者可以生产块级作用域;var的创建和初始化被提升,赋值不会被提升;在非全局作用域内,let的创建被提升,初始化和赋值不会被提升;function的创建、初始化和赋值均会被提升。

2. JS函数调用栈(函数调用,栈结构)又称执行上下文栈

按代码块,分为全局代码和函数体内代码。从全局执行上下文开始,边执行边去该执行上下文取出并编译下一个调用栈的函数代码生成新的执行上下文和可执行代码,并把新的执行上下文放进调用栈顶端;当函数执行结束返回时,将该函数的执行上下文从调用栈(执行上下文栈)中取出并返回正确值,并销毁该上下文环境。可以通过浏览器source标签断点查看调用栈也可以console.trace()打印输出。

3. 栈溢出及栈溢出避免/递归函数改写

  • 栈溢出是指“当前任务”执行上下文个数/内存超出一定数目(最大栈调用大小)。
  • 把递归函数调用改成其他形式,改成循环迭代语句(不会创建新的执行上下文)
  • 加入定时器或者Promise,把当前任务拆分成其他任务(宏任务/微任务),因为微任务或者宏任务结束即回收
  • 尾调用优化(es6新特性):www.ruanyifeng.com/blog/2015/0…

尾调用是指函数的最后一步是调用另一个函数。函数调用栈的设计初衷之所以在函数内部调用函数会将新的函数执行上下文入栈的同时保留上一级执行上下文是因为该函数不是外层函数的最后一步,后续代码有可能仍需访问外层执行上下文的变量;而一旦是尾调用即意味着这是函数的最后一步,创建新的执行上下文并入栈的同时无需保留上级执行上下文,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了,所以可以把他销毁;这叫尾调用优化,即只保留内层函数的调用记录,如果所有函数都是尾调用,完全可以做到每次调用记录只有一项,大大节省内存。

尾递归是指函数最后一步是调用自身。通过给递归函数增加一个参数的方式改成尾递归。

这是函数式编程语言的特点,函数式编程语言支持尾递归优化,ES6第一次将这个写入语言规格,要求所有ES的实现都必须部署“尾调用优化”。也就是说,在支持ES6中的宿主环境(node或者浏览器他们的编译器,目前只有safari部署了尾调用)中使用尾调用就不会发生栈溢出。但这一特性也只在ES6的严格模式下有效。

4. 作用域链

变量查找

每个执行上下文的变量环境中都包含了一个外部引用,用来指向外部的执行上下文(outer);会先在当前执行上下文查找,找不到则到outer指向的上下文中查找,而不是简单的按照上下文栈由上到下顺序查找;形成作用域链。作用域链是由词法作用域决定的,是由声明时在代码中的位置决定的,是代码阶段就决定好的,和函数怎么调用没有关系,是一种静态作用域。

5. 闭包

  • 原因:根据词法作用域规则,内部函数总是可以访问外部函数(作用域链中的outer)中的变量,所以哪怕外部函数已经执行结束(出栈并被销毁)但内部函数仍然要可以访问使用到的外部函数的变量,该变量仍会单独存在于内存,仅可被该内部函数访问,称为形成了外部函数的闭包。
  • 当函数嵌套时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局作用域下可访问时,就形成了闭包。
  • 可以把全局环境理解为全局匿名函数的执行上下文,那么所有函数都是闭包,都可以访问全局变量,只是全局函数不会提前出栈而已。
  • 用法:如果该闭包一直使用,可以赋值为全局变量;相反,赋值为局部变量。

6. this

变量查找

  • 需求点:在对象内部的方法中使用对象内部的属性

  • 作用域链机制

    var bar =
    {    myName:"time.geekbang.com",
    
        printName: function () {
    
            console.log(myName)
    
        }    
    
    };
    
    function
    foo() {
    
        let myName = "极客时间"
    
        return bar.printName
    
    };
    
    let
    myName = "极客邦"
    
    let _printName
    = foo()
    
    _printName()
    
    bar.printName()
    
    //极客邦
    //极客邦
    
  • this机制

    var bar =
    {    myName:"time.geekbang.com",
    
        printName: function () {
    
            console.log(this.myName)
    
        }    
    
    };
    
    function
    foo() {
    
        let myName = "极客时间"
    
        return bar.printName
    
    };
    
    let myName
    = "极客邦"
    
    let
    _printName = foo()
    
    _printName()
    
    bar.printName()
    
    //undefined
    //time.geekbang.com
    
  • this是和作用域链完全不同的两套查找变量的系统,无任何联系

  • this也是和执行上下文绑定的,每个执行上下文都有自己的this。由于执行上下文有三类(全局,函数,eval),this也有三类。全局执行上下文的this指向window对象,作用域链最低端也包含window对象,这是作用域链与this唯一的交集;函数执行上下文中的this非严格模式下也是默认指向window,打破了数据的边界,容易造成误操作,严格模式下默认指向undefined,可以通过三种方法改变函数执行上下文中的this指向。

  • 设置函数执行上下文中的this指向

  1. call,apply,bind函数:以call为例

    let bar =
    {  myName : "极客邦",
    
      test1 : 1
    
    }
    
    function
    foo(){
    
      this.myName = "极客时间"
    
    console.log(this)
    
    }
    foo.call(bar);
    console.log(bar)
    
  2. 对象调用:使用对象调用其内部的对象方法,该方法的this指向该对象;原理类似于对这个对象方法也执行了call只不过call的参数是该对象本身。但是如果将对象方法赋值给全局变量并在全局调用,则函数内部的this又会指向全局window。

  3. 通过构造函数中设置:构造函数中的this指向通过new关键字创建的对象本身。

    function
    CreateObj(){  this.name = "极客时间"
    
    }
    
    var myObj = new CreateObj()
    
    等价于
    
    var
    tempObj = {}
    
    CreateObj.call(tempObj)
    
    return
    tempObj
    
  4. 通过以上的三种情况,我们可以理解为可以改变this指向的,实际上都是通过call函数来底层实现。

  5. this缺陷:嵌套函数中的this不会从外层得到继承,而是默认指向window;可以对this重新赋值来实现“this继承”,该方法的实质是将变量查找的this体系转化为作用域链体系,但在内部嵌套函数内访问this,this指向的还是window,这一特性并没有改变。也可以使用ES6的箭头函数,因为箭头函数并不创建其自身的执行上下文,所以变量都还是外层的,和外层保持一致。

    var myObj
    = {  name : "极客时间", 
    
      showThis: function(){
    
        console.log(this)
    
        function bar(){console.log(this)}
    
        bar()
    
      }
    
    }
    
    myObj.showThis()
    

7. 原型与原型链

属性查找

  • 一个对象实例存在两部分属性:实例属性(构造函数定义的属性,可用hasOwnProperty判断),原型属性(原型链上定义可用于继承的属性)
  1. 实例对象共享原型上面的属性和方法
  2. 实例自身的属性会覆盖原型上的同名属性,自身不存在的属性才会去原型上查找
  3. 原型本身也是个对象,可以重写,但会对所有实例有效,但对于在改写前和改写后创建的实例对象的表现却有不同。
  4. 为什么要使用原型属性?将公用属性和方法共享,也就共享了这个变量及他所占的内存。
  • (构造函数的)显式原型(prototype):其中的contructor指向构造函数自身;但由于显示原型本身也是个对象实例,其隐式原型指向其构造函数的显式原型
  • 构造函数本身也是个实例,是Function构造函数的实例
  • (实例对象的)隐式原型(proto):指向构造函数的显示原型

8. 继承

  • es6的类继承:class定义时使用extends关键字
  • es5的继承:

1.原型继承:将子类的原型对象指向父类实例

// 父类
function Parent() {
    this.a = 1
}
// 父类的原型方法
Parent.prototype.get = function() {
    return this.a
}
// 子类
function Child() {}

// 让子类的原型对象指向父类实例,子类实例在子类中找不到的属性和方法就会到上原型(父类实例)寻找
Child.prototype = new Parent()
Child.prototype.constructor = Child 
// 根据原型链的规则,改写原型,构造函数会指向Object,需要重新绑定constructor

const child = new Child()
child.a
child.get()

缺点是:由于所有Child实例原型都指向同一个Parent实例, 因此对某个Child实例的父类引用类型变量修改会影响所有的Child实例;在创建子类实例时无法向父类构造传参, 即没有实现super()的功能

2.构造函数继承:在构造函数中使用call方法,改变运行时this的指向;既能避免实例之间共享一个原型实例,又能向父类构造方法传参;

//父类
function Parent(value) {
    this.a = value
}
// 父类的原型方法
Parent.prototype.get = function() {
    return this.a
}
// 子类
function Child() {
    Parent.call(this,2)
}

const child = new Child()
child.a//2
child.get()//报错

缺点是继承不到父类的原型上的属性和方法。

3.组合式继承(结合原型继承和构造函数继承)

//父类
function Parent(value) {
    this.a = value
}
// 父类的原型方法
Parent.prototype.get = function() {
    return this.a
}
// 子类
function Child() {
    Parent.call(this,2)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child 
const child = new Child()

缺点:每次创建子类实例都执行了两次构造函数(Parent.call()new Parent()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅

4.寄生组合式继承

首先为了避免有两份相同属性和方法,我们将子类原型指向父类实例改为指向父类原型,但这样子类和父类公用同一份原型,子类修改原型,父类也会受到影响;我们再改为将子类原型指向父类原型的一份浅拷贝(Object.create())

//父类
function Parent(value) {
    this.a = value
}
// 父类的原型方法
Parent.prototype.get = function() {
    return this.a
}
// 子类
function Child() {
    Parent.call(this,2)
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child 
const child = new Child()

此为目前最成熟的ES5继承方式,babel对ES6继承的转化也是使用了寄生组合式继承。

juejin.cn/post/684490…中ES5如何实现继承很详细。

9. 事件循环

JS是单线程语言。拥有唯一的一个事件循环机制,主线程按照这个逻辑执行任务。

  • 任务分类:
  1. 宏任务(Event Queue):整体代码,setTimeout,setInterval,I/O
  2. 微任务(Event Queue):Promise.then,await后面的代码,process.nextTick(node中)
  3. 异步任务与同步任务:当异步任务执行完成,其异步任务的回调函数也会立即放到对应宏任务/微任务的Event Queue里(放到宏任务队列的就相当于一个新的宏任务,而放到微任务队列的就相当于一个新的微任务了)
  • 执行顺序:
  1. 先执行宏任务,再执行微任务;宏任务和微任务各自维护自己的事件队列;但宏任务和微任务中都有可能遇到异步任务。
  2. 同步任务进入主线程;异步任务进入Event Table并注册回调函数,当异步任务完成后将回调函数放入对应的Event Queue中;当同步任务执行完成,会从事件队列中读取下一个任务;回调函数中可能还存在不同任务,以此循环执行。

10. es6的let,const与es5中var的区别,用es5 var怎么写

  • 区别:var存在变量提升;let/const会形成块作用域,不能跨块访问,会形成暂时性死区,无法在声明前访问变量,var不会;let/const不能声明同名变量(预编译时报错),var可以;在全局环境下定义的函数和变量(var)会成为window对象的方法和属性,而使用let/const定义的变量不会;

  • let:使用babel转码发现,对let声明的变量,仍使用var声明,只是名称前加下划线区分而已,其并没有严格实现let/const(仍然可以挂载到window),我们可以采用立即执行函数,使用立即函数包裹,函数执行完随即销毁,使得别处无法访问,达到块作用域的目的。

    (function testBabel(){    const a = 3;    for(let i = 0;i<a;i++){        console.log(i)    }    console.log('a',a)    console.log('i',i)})()
    
  • const:const只是保存变量的地址不变,对于对象引用,也只是地址不变,对象属性是可以改变的。我们可以使用Object.defineProperty或者Proxy或者Object.freeze()来实现。

    function _const(data,value){    window.data = data;    Object.defineProperty(window,data,{        enumerable:false,//不可for...in或Object.keys()枚举,模拟一种没有挂载在window的假象        configurable:false,//不可delete和更改writable,value属性        // value:value,        // writable:false,        get:function(){value:data            return value;        },        set:function(data){            if(data !== value){                throw new Error('Assignmet to constant variable.')            }else{                return value;            }        }    })}_const('obj',{a:1});obj.a = 2;console.log(obj)delete objconsole.log(obj)var a = 1;for(item in window){    if(item === 'obj'){        console.log(window[item])    }}obj = {b:2}
    

11. 用es5实现私有变量(闭包使用)

  • 构造函数中的属性方法:形成的闭包可以访问构造函数属性

my.oschina.net/bob1900/blo…

  • 立即执行函数中使用函数表达式声明一个“全局”的构造函数,并在原型上添加方法

my.oschina.net/bob1900/blo…

12. JS数据类型及其判断

共8种数据类型:

  • 6种原始类型(string,number,boolean,null,undefined,symbol):值存储,是没有函数可以调用的,会存在类型强制转换,比如将string类型转换成String对象类型,才可以调用String对象类型的系列方法;此外,number类型是浮点类型的,存在0.1+0.2!=0.3诸如此类的情况;
  • 1种对象类型object(String,Array,Function,``Date等除了原始类型的其他类型):地址指针存储
  • 1种ES6新增类型bigInt:

数据类型判断:

  • typeof:单目运算符,判断某个变量是何数据类型,局限在于对除Function对象以外的其他对象类型一律返回object,对null返回object是个历史bug。返回值只是如下固定几个。

  • instanceof:双目运算符,查找变量构造函数原型,判断某一变量是否是某个对象的实例,包括自定义对象,原理是通过原型链判断。

    function p(){};
    a = new p();
    a2 = new p();
    a instanceof p;
    a instanceof Object;
    b = [];
    b instanceof Array;
    b instanceof Object;//父类可判断
    c = new Date();
    c instanceof Date;
    c instanceof Object;
    function p2(){};
    p2.prototype = new p();//继承可判断
    d = new p2();
    d instanceof p;
    

补充注意点

//函数参数arguments非数组类型,可以使用...展开
function a(){
    console.log(arguments,typeof arguments,arguments instanceof Array)
};
a(1,2);
// 'object' false

//普通变量通过字面量生成的使用instanceof判断均为false,因为并不是通过对象实例生成的
n1 = new Number(2);
n2 = 2;
n1 instanceof Number//true
n2 instanceof Number//false

13.使用var定义的全局变量和使用window定义的全局变量的区别

  • 当前访问一个不确定下文是否声明了的变量。若未使用var声明,直接访问则报错,使用window访问则为undefined
  • 使用var声明的全局变量使用delete命令无效,而window属性有效;原因是前者默认configurable为false,
  • 在函数内声明的var变量为局部变量,如果想要外部也可以访问,需定义到window下

14. 页面优化:代码重构优化,页面性能优化

15. 手写promise.all

16. 设计模式:工厂模式(Jquery),建造者模式(Vue)

17. MVC与MVVC:

17.防抖和节流:手写

18.交换内容

function swap(arr,index1,index2){
    [arr[index1],arr[index2]] = [arr[index2],arr[index1]];
}
衍生有:
function swap(obj1,obj2){
    [obj1,obj2] = [obj2,obj1];
}

19.自定义属性

//定义
<div id = 'test' data-test = 'hhhh'></div>
//使用
document.getElementById('test').dataset.test//hhhh

20.getXXXByXXX与querySelector与$()返回内容比较

21.模块化方案commonJS,CMD,AMD

22.服务,指令,组件区别

23.setTimeout第三个参数

setTimeout第三个参数会被作为第一个参数函数的参数传入使用。

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}
// 6 6 6 6 6

for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}
// 1 2 3 4 5

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}
// 1 2 3 4 5

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}
// 1 2 3 4 5

24.深浅拷贝

浅拷贝

只会拷贝所有的属性值到新的对象中。

  • Object.assign
  • ...

深拷贝

拷贝所有属性及属性的属性到新的对象中。

  • JSON.parse(JSON.stringify()) :缺陷在于忽略undefined,symbol,函数,不能解决循环引用的对象

  • MessageChannel:缺陷在于无法处理函数

    function structuralClone(obj) { return new Promise(resolve => { const { port1, port2 } = new MessageChannel() port2.onmessage = ev => resolve(ev.data) port1.postMessage(obj) }) }

    var obj = { a: 1, b: { c: 2 } }

    obj.b.d = obj.b

    // 注意该方法是异步的 // 可以处理 undefined 和循环引用对象 const test = async () => { const clone = await structuralClone(obj) console.log(clone) } test()

  • 自定义deepClone

    function deepClone(obj) { function isObject(o) { return (typeof o === 'object' || typeof o === 'function') && o !== null }

    if (!isObject(obj)) { throw new Error('非对象') }

    let isArray = Array.isArray(obj) let newObj = isArray ? [...obj] : { ...obj } Reflect.ownKeys(newObj).forEach(key => { newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] })

    return newObj }

    let obj = { a: [1, 2, 3], b: { c: 2, d: 3 } } let newObj = deepClone(obj) newObj.b.c = 1 console.log(obj.b.c) // 2

25.visibilitychange 监听H5页面激活失活状态

github.com/CYLpursuit/…

26.requestAnimationFrame动画

github.com/CYLpursuit/…