从原型到原型链
前面主要大概了解了一些原型链的一些知识,这里重点深化一下
prototype
每个函数都有一个 prototype 属性
函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型
这也就构成了第一层关系
proto
每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型
这样我们就可以更新第二层关系
constructor
既然对象可以指向他的原型,那么同样的原型也可以指向该对象
每个原型都有一个 constructor 属性指向关联的构造函数
![屏幕截图 2024-05-27 202454](C:/Users/PC/Pictures/Screenshots/屏幕截图 2024-05-27 202454.png)
ok,更新我们的关系图
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取
1 | person.constructor === Person.prototype.constructor |
原型也是一个对象, 也就是说原型是通过 Object 构造函数生成的,那么我们就可以再次更新我们的关系图
那么我们很容易就会去想到Object.prototype 的原型是什么
Object.prototype._proto_ 的值为 null 也就是说Object.prototype 没有原型,所以当查找属性的时候查到 Object.prototype 就可以停止查找了
我们再来跟新我们的关系图
这里简单的通过图来理解了原型链,原型链的查找规则也很有意思
其优先查找实例,当实例中无法查询到,就去查询其原型
作用域
作用域是指程序源代码中定义变量的区域,作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限
JavaScript采用静态作用域,也就是说执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1
ok,那我们来看一个很有意思的点
JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置
JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效
JavaScript执行
JavaScript的执行也是比较有特色的
我们先通过代码来简单交接一下
两端作业相同的代码执行结果却截然不同
其原因就是在于 JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行
要解释清楚这个,就需要先了解JavaScript 的可执行代码,即全局代码、函数代码、eval代码,而当执行到一个函数代码的时候,就会执行上下文栈,也就是说第一段代码是在执行一个函数代码,而第二段代码则是在执行一个全局代码
这里我们理解一下一个概念,也就是上下文栈,其实也很好理解,如字面意思,其本质上是一个栈,有着栈特有的特性,先进后出
上下文中有三个比较重要的概念
变量对象 作用域链 this
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明
这里就会有几个概念的上下文
全局上下文
全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性
在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询
全局上下文中的变量对象就是全局对象
函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象
活动对象和变量对象其实是一个东西,但可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活
解释明白这几个概念后,这里我们就可以来理解 JavaScript中代码的运行过程了
- 进入执行上下文
- 代码执行
执行上下文时,会去检查变量对象的所有形参 ,函数声明以及变量声明
在代码执行阶段,根据代码逻辑,会再次修改变量对象的属性值
明白了变量对象,我们就来看下一个点,作用域链
我们了解到在查找对象的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,而这也就是作用域链
1 | function foo() { |
这是一个很简单的函数定义,那么我们来看他的作用域链
1 | foo.[[scope]] = [ |
然后我们激活函数,进入函数上下文后,VO/AO 被创建就会将活动对象添加到作用链的前端
1 | [AO].concat([[Scope]]) |
ok,我们用一个简单的实例来加深理解
1 | var scope = "global scope"; |
checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
1 | checkscope.[[scope]] = [ |
执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
1 | ECStack = [ |
checkscope 函数并不立刻执行,开始做准备工作
第一步:复制函数[[scope]]属性创建作用域链
1 | checkscopeContext = { |
第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
1 | checkscopeContext = { |
第三步:将活动对象压入 checkscope 作用域链顶端
1 | checkscopeContext = { |
准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
1 | checkscopeContext = { |
查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
1 | ECStack = [ |
this
这里就比较有意思了
1 | 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref) |
这里需要解释几个函数
GetBase 返回 reference 的 base value
IsPropertyReference 如果 base value 是一个对象,就返回true
MemberExpression简单理解 其实就是()左边的部分
调用 GetValue,返回的将是具体的值,而不再是一个 Reference
Reference 这个用例子来看会更好理解
1 | var foo = 1; |
1 | var foo = { |
这里我们一个一个来理解
foo.bar()
我们先看到他的Reference
1 | var Reference = { |
根据我们之间给的条件
如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
1 | this = GetBase(ref), |
也就是说 this 的值就是 foo
(foo.bar)()
foo.bar 被 () 包住,但实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的
(foo.bar = foo.bar)()
存在赋值运算符,即调用GetValue,所以返回的值不是 Reference 类型
如果 ref 不是 Reference,那么 this 的值为 undefined
this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象
(false || foo.bar)()
存在逻辑运算,即调用GetValue,所以返回的值不是 Reference 类型
如果 ref 不是 Reference,那么 this 的值为 undefined
this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象
(foo.bar, foo.bar)()
逗号操作符
因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined
我们简单总结一下就是用到各种操作符后,都将调用到 GetValue,导致返回值为非Reference 类型,this 即为 undefined
闭包
闭包是指那些能够访问自由变量的函数
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
闭包 = 函数 + 函数能够访问的自由变量
1 | var a = 1; |
foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。
那么,函数 foo + foo 函数访问的自由变量 a 就是构成了一个闭包
这里我们补充一个定义
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
用一个例子来加深对闭包的理解
这里就很好的看出闭包的作用
先来解释前面一段
当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
1 | data[0]Context = { |
也就是说,当执行到 data[0] 函数的时候,函数已经从上下文栈弹出,所有会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3
data[1] 和 data[2] 是一样的道理
第二段当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
1 | data[0]Context = { |
data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,所以打印的结果就是0
data[1] 和 data[2] 同理