笔记:javascript 深入理解
JavaScript深入之从原型到原型链
构造函数->原型
每个函数都有一个 prototype 属性,指向实例的原型
原型:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型
实例->原型
每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型
ES5的方法,可以获得对象的原型
(Object.getPrototypeOf(person) === Person.prototype person是实例,Person是构造函数
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
原型->构造函数
每个原型都有一个 constructor 属性指向关联的构造函数
实例-> 构造函数
person.constructor === Person 从原型上面继承
Null代表什么
null 表示“没有对象”,即该处不应该有值。
obj.__proto__ :非标准的方法访问原型,并不存在于 Person.prototype 中,并不是原型上的属性,它是来自于 Object.prototype,返回了Object.getPrototypeOf(obj)
继承:
继承意味着复制操作, 默认并不会复制对象的属性,在两个对象之间创建一个关联,通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
JavaScript深入之词法作用域和动态作用域
作用域:定义变量的区域,确定当前执行代码对变量的访问权限
Js: 词法作用域(lexical scoping),也就是静态作用域,函数的作用域基于函数创建的位置
静态作用域:作用域在函数定义
动态作用域:作用域是在函数调用
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); 思考:结果一样,有什么不同
JavaScript深入之执行上下文栈
当执行一段代码的时候,会进行一个“准备工作
这个“一段一段”中的“段”究竟是怎么划分的呢
Js引擎遇到一段怎样的代码时才会做“准备工作”呢?
执行到一个函数的时候,就会进行准备工作,就叫做"执行上下文(execution context)"
Js 的可执行代码(executable code)的类型
全局代码、函数代码、eval代码
执行上下文栈(Execution context stack,ECS
管理创建执行上下文 ECStack = []
Js解释执行代码,最先遇到全局代码,so初始化首先就会向执行上下文栈压入一个全局执行上下文, globalContext ,只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前ECStack 最底部永远有个 globalContext
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出
函数执行结束之后,如果没有显示地返回值,默认是undefined,chrome中会把函数执行的结果打印出来
JavaScript深入之变量对象
执行上下文,都有三个重要属性
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
变量对象:数据作用域,存储了在上下文中定义的变量和函数声明
不同上下文变量对象不同:全局上下文下的变量对象和函数上下文下的变量对象
全局上下文中的变量对象:全局对象
全局对象:
预定义的对象 访问所有其他所有预定义的对象、函数和属性 顶层 JavaScript 代码中,可以用关键字 this 引用全局对象, 全局对象是作用域链的头 全局对象是由 Object 构造函数实例化的一个对象
全局上下文中的变量对象:活动对象(activation object, AO)
活动对象和变量对象其实是一个东西
它们其实都是同一个对象,只是处于执行上下文的不同生命周期
变量对象:规范上的实现,不可在 JavaScript 环境中访问
活动对象:只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活而只有被激活的变量对象也就是活动对象上的各种属性才能被访问
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象
JS 引擎在分析代码的时候,分为两个阶段:编译阶段和执行阶段
- 编译阶段:处理声明语句,所有声明的变量添加为当前执行上下文变量对象(VO)的属性。如果是变量声明,其值暂且初始化为 undefined,如果是函数声明,它会在堆上开辟内存,并将函数定义放到堆上,函数变量只保存这指向函数定义的地址。
- 执行阶段:编译结束后,JS 引擎会再次扫描代码,这个阶段主要的任务是根据执行语句,更新变量对象等。
当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:
函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值设为 undefined
函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
变量声明
- 由名称和对应值(undefined)组成一个变量对象的属性被创建;
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包括 Arguments 对象
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
- 在代码执行阶段,会再次修改变量对象的属性值
补充:
Arguments对象 - 调用函数时,会为其创建一个Arguments对象,并自动初始化局部变量arguments,指代该Arguments对象。所有作为参数传入的值都会成为Arguments对象的数组元素
执行上下文的生命周期
- 创建阶段
在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
- 代码执行阶段
创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码
AO 实际上是包含了 VO 的
也就是说 AO 的确是在进入到执行阶段的时候被激活,但是激活的除了 VO 之外,还包括函数执行时传入的参数和 arguments 这个特殊对象。
AO = VO + function parameters + arguments
在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
在进入执行上下文阶段,只会将有 `var,function修饰的变量或方法添加到变量对象中。在进行执行阶段前一刻,foo和bar方法的它们的VO中均没有a属性。在执行阶段,执行到 a= 1时,才将a变量添加到全局的变量对象中而不是在进入执行上下文阶段。所以foo方法中会报错,bar方法会打印 1。
function foo() { console.log(a); a = 1; } foo(); // Uncaught ReferenceError: a is not defined function bar() { a = 1; console.log(a); } bar(); // 1 WT: let/const 在esc中的表现
JavaScript深入之作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
函数创建:
函数的作用域在函数定义的时候就决定了
函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中
[[scope]] 就是所有父变量对象的层级链([[scope]] 并不代表完整的作用域链!)
function foo() { function bar() { ... } } 创建时 foo.[[scope]] = [ globalContext.VO ]; bar.[[scope]] = [ fooContext.AO, globalContext.VO ];
函数激活:
当函数激活时,进入函数上下文,创建 VO/AO 后,将活动对象添加到作用链的前端
这时候执行上下文的作用域链,我们命名为 Scope:
Scope = [AO].concat([[Scope]]);
e.g.:
var scope = "global scope"; function checkscope(){ var scope2 = 'local scope'; return scope2; } checkscope();
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [ checkscopeContext, globalContext ];
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = { Scope: checkscope.[[scope]], }
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], }
5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] }
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] }
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [ globalContext ];
补充:
每一个函数都有自己的执行环境,函数执行的时候,会创建函数执行上下文
在源代码中当你定义(书写)一个函数的时候(并未调用),js引擎也能根据你函数书写的位置,函数嵌套的位置,给你生成一个[[scope]],作为该函数的属性存在(这个属性属于函数的)。即使函数不调用,所以说基于词法作用域(静态作用域)。
然后进入函数执行阶段,生成执行上下文,执行上下文你可以宏观的看成一个对象,(包含vo,scope,this),此时,执行上下文里的scope和之前属于函数的那个[[scope]]不是同一个,执行上下文里的scope,是在之前函数的[[scope]]的基础上,又新增一个当前的AO对象构成的。
函数定义时候的[[scope]]和函数执行时候的scope,前者作为函数的属性,后者作为函数执行上下文的属性。