Better

Ethan的博客,欢迎访问交流

JavaScript原型链、作用域与上下文

最近在看一个github大神写的博客,看完之后有醍醐灌顶的感觉,前端迭代太快,新的框架层出不穷,我觉得学习不应该只是懂得一堆API该如何使用,更应该去了解一些底层原理,有句话我觉得说的很对,框架能够让我们跑的更快,但只有了解原生的JS才能让我们走的更远。最近准备放下react或者vue等知识的学习,静下心来沉淀这些底层的东西,因为学这个的感觉比学习API让人激动多了。

声明

原型和原型链

  • 来源:原型和原型链
  • prototype
    • prototype是函数才会有的属性
    • 函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1和 person2的原型。
  • __proto__
    • 这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。
      console.log(person.__proto__ === Person.prototype); // true
      
    • 来源于Object.prototype
  • constructor
    • 每个原型都有一个 constructor 属性指向关联的构造函数
      console.log(Person === Person.prototype.constructor); // true
      
    • 实际开发中可能看到person.constructor的用法,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性
  • 继承

    《你不知道的JavaScript》:继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

  • 实例与原型

    • 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
  • 原型的原型
    • 实例原型也是一个对象,也具有__proto__属性指向构造函数的prototype
  • 原型链
    • 顶层对象Object的prototype的实例呢?
      console.log(Object.prototype.__proto__ === null) // true
      
    • 这张图片太经典,忍不住想保存 prototype.png

词法作用域

  • 来源:词法作用域
  • JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
  • 词法作用域:函数的作用域在函数定义的时候就决定了。
  • 动态作用域:函数的作用域是在函数调用的时候才决定的。
  • 《JavaScript权威指南》

    JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里,其中的变量 scope一定是局部变量,不管何时何地执行函数f(),这种绑定在执行f()时依然有效。

  • 理解这个是理解闭包的基础。

执行上下文栈

  • 来源:执行上下文栈
  • 变量提升:变量在声明的时候,会将所有变量在作用域头部进行前置声明,但是赋值保留在原位置。
  • 函数提升:函数声明时,会自动将函数前置声明,这也是为什么函数后声明,但是前面可以调用的原因,也不存在类似C语言的手动前置声明。
  • 思考,如果同时存在函数声明和变量声明,并且名称一样,会怎么处理呢?
    • 因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
  • JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个准备工作,比如函数提升和变量提升。那么具体遇到什么代码会进行准备工作呢?全局代码函数代码eval代码
  • 执行上下文栈: JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
    • 工作原理:底部永远有全局执行上下文(globalContext),当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

执行上下文

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

变量对象

来源:变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

  • 全局上下文
    • 在顶层JavaScript代码中,可以用关键字this引用全局对象。
    • 在顶层JavaScript代码中声明的所有变量都将成为全局对象的属性。
  • 函数上下文

    • 在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

      活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

    • 执行上下文的代码处理分成两个阶段进行处理:分析和执行

      • 分析:活动对象AO的组成有:arguments (初始化阶段)+ 所有形参(分析阶段) + 函数声明(分析阶段) + 变量声明(分析阶段)
      • 执行:在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

作用域链

来源:作用域链
为什么函数的作用域在函数定义的时候就决定了呢?从作用域链的角度可以更加理解。创建和激活两个时期来讲解作用域链是如何创建和变化的

  • 函数创建
    • 函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
  • 函数激活
    • 当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
      Scope = [AO].concat([[Scope]]);
      
  • 从checkscope函数执行上下文中作用域链和变量对象的创建过程
    1. 函数被创建,保存作用域链到 内部属性[[scope]]
    2. 开始执行函数,创建函数执行上下文,函数执行上下文被压入执行上下文栈
    3. 准备工作
      • 第一步:复制函数[[scope]]属性创建作用域链
      • 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
      • 第三步:将活动对象压入 checkscope 作用域链顶端
        checkscopeContext = {
        AO: {
         arguments: {
             length: 0
         },
         scope2: undefined
        },
        Scope: [AO, [[Scope]]]
        }
        
    4. 执行函数,随着函数的执行,修改 AO 的属性值
      checkscopeContext = {
      AO: {
         arguments: {
             length: 0
         },
         scope2: 'local scope'
      },
      Scope: [AO, [[Scope]]]
      }
      
    5. 函数执行完毕,函数上下文从执行上下文栈中弹出

this

来源:this
看第一遍,智商不够,完全看不懂,选择不说话。第二遍,懂一丢丢,总结两句。

  • Types
    • ECMAScript 的类型分为语言类型和规范类型。
    • ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。
    • ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。
  • Reference

    这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。

    • 组成
      • base:属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。
      • name:属性的名称
      • strict:严格默认?不懂
    • 方法:
      • GetBase:得到Reference的base值
      • IsPropertyReference:如果 base value是一个对象,就返回true。
      • GetValue: 从 Reference 类型获取对应值的方法,调用 GetValue,返回的将是具体的值,而不再是一个 Reference,这个很重要!
      • ImplicitThisValue:该函数始终返回 undefined。
  • 那么this和Reference的关系?如何确定this值呢?
    • 计算 MemberExpression的结果赋值给 ref
      • 简单理解MemberExpression 其实就是()左边的部分。
    • 判断 ref 是不是一个 Reference 类型
      1. 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
      2. 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
      3. 如果 ref 不是 Reference,那么 this 的值为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。
    • 规则列出来了,那么判断 ref 是不是一个 Reference 类型。关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。
      1. 属性访问方式(foo.bar):ref是一个Reference类型。
      2. 分组操作符((express)):最终ref的类型根据组内表达式决定。如果依旧是属性访问方式,则依旧是Reference类型,更多情况下面详细说明。
      3. 组内是赋值操作:会调用GetValue函数,返回的将是具体的值,不再是Reference
      4. 组内逻辑与算法:会调用GetValue函数,返回的将是具体的值,不再是Reference
      5. 组内逗号操作符:会调用GetValue函数,返回的将是具体的值,不再是Reference
      6. 最简单情形:foo(),此时Reference 类型如下,看了类型就知道怎么分析了,属于第2种情况:
        var fooReference = {
        base: EnvironmentRecord,
        name: 'foo',
        strict: false
        };
        

执行上下文的具体处理过程

  1. 执行全局代码,创建全局执行上下文并压入执行上下文栈。
  2. 全局上下文初始化,三部分:VO,Scope(作用域链),this
  3. 某函数A被创建,保存作用域链到函数的内部属性[[scope]]
  4. 执行A函数,创建函数执行上下文并压入执行上下文栈
  5. 函数执行上下文初始化,四步骤
    1. 复制函数 [[scope]] 属性创建作用域链,三部分:AO,Scope(作用域链),this
    2. 用 arguments 创建活动对象
    3. 初始化活动对象,即加入形参、函数声明、变量声明
    4. 将活动对象压入 checkscope 作用域链顶端
  6. 如果函数内部又创建了函数,则重复3-5步骤
  7. 函数执行完毕,函数上下文从执行上下文栈中弹出


留言