前端之旅:总结 this、闭包、作用域作用域链、原型和原型链

其实这篇总结问应该是在任务三的开始部分就完成的,但我一开始就投入了代码的怀抱,迟迟不肯动手,直到在任务三完成之后,并且在摸索任务四的时候才每天留了点时间将其完成。


这几个js中的难点几乎每天都会准时出现在各大技术网站上,这让人不得不冲动自己也来总结一番,趁还没忘记最近看的一堆参考资料。
我试图用最简洁的方式交一份满意的答卷。

this

this在大多数OO语言中都是指当前对象引用,而在js中,却有几种不同的表现:
《js语言精粹》中明确指出this的值取决于调用的模式,js中一共有4中调用模式:

  1. 方法调用模式,一个函数作为对象的方法被调用时,this被绑定到该对象
  2. 函数调用模式,一个普通函数(非方法)被调用时,this绑定到全局对象,浏览器中即window
  3. 构造器调用模式,在一个函数前带上new关键字来调用,内部会创建一个连接到该函数原型的新对象,同时this被绑定到那个新对象上
  4. apply调用模式,js提供了apply和call两个方法来更改this的绑定对象

最常见的困惑是一个对象方法返回一个函数,其实这个时候的函数已经属于全局对象了,所以执行返回的函数中的this指向全局对象。
在给DOM绑定事件时候的this时也容易让人困惑,可以从鸟哥的 深入理解Javascript之this关键字 文中寻找答案。

注意事项:

  • this始终指向此时函数/方法调用者,而不是在定义函数/方法时指向的对象。
  • this是执行上下文环境的一个属性,而不是某个变量对象/活动对象的属性。
    这个特点很重要,因为和变量不同,this是没有一个类似搜寻变量的过程。当你在代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻。this的值只取决于进入上下文时的情况。

闭包

闭包是什么呢?见名知意,即一个封闭的环境,即使用专业术语描述也相差无几。
闭包的作用是用来保存函数的执行上下文方便以后使用。
常见的闭包使用如 IIFE(立即执行表达式)、函数的柯里化,模块模式。
那么闭包在哪呢,通常闭包的形成都是在一个函数中返回一个函数的时候,由于返回的函数往往引用了父函数的上下文环境,导致父函数执行完成后内存没有得到回收/释放(引用计数机制),所以在返回的函数中可以访问父函数的上下文环境。这就形成了闭包。
上一个简单的示例:

1
2
3
4
5
6
7
8
9
var sum = (function(){
var count = 0;
return function(n) {
count += n;
return count;
}
})();
sum(5); //5
sum(2); //7

示例中在父函数初始化了count变量,返回的函数中引用了count,sum保存了返回函数,在以后调用sum的过程中,count会始终存在并保存上次调用后的值,那么count存放在哪里的呢,它就是被存放在了返回的函数形成的闭包中(上下文环境)。关于是如何在返回的函数中执行的时候找到count变量的过程,那么就涉及到接下来要讲的作用域和作用域链了。

注意事项:

  • 由于每个标准函数在创建时候保存了[[Scope]],所以理论上来讲,ECMAScript中的_所有函数都是闭包。
    闭包是一个代码块(在ECMAScript中是一个函数)和以静态方式/词法方式进行存储的所有父作用域的一个集合体。所以,通过这些存储的作用域,函数可以很容易的找到自由变量。

作用域作用域链

作用域是?
简单的说,作用域就是变量与函数的可访问范围,即作用域控制着标识符(变量/函数/形参等等)的可见性和生命周期。在JavaScript中,标识符的作用域只有全局作用域和函数作用域两种。没有用var声明的都属于全局作用域,if/for/while等语句块没有内部作用域,在其中声明的变量将属于当前函数的作用域。

那么作用域链是什么呢?
在定义函数的时候该函数内部会有一个[[scope]]属性,它指向定义该函数的函数的作用域链,这个链的底端是全局对象,顶端是当前活动对象。
作用域链的形成是从函数调用开始的,首先在全局环境中,当前活动对象就是GO,当执行一个函数的时候,会为该函数创建一个执行上下文(excution context),在执行上下文中有初始化后的this,活动对象AO、变量对象VO等属性,然后将活动对象加入到作用域链顶端,加入的过程可能是在[[scope chain]]中加入,也可能是直接链接到__parent__。因为我看过的资料关于这个链的实现的描述大概分两种:

  • js高程是一种类似数组的方式并且按函数调用栈的顺序保存其“作用域”的引用,如图
    scope_chain实现
  • 其他资料也有描述的是一个__parent__的内部属性来维护“父作用域”的引用,比如下图
    parent实现

虽然两种方式都可行,但是参照js原型链的实现方式,可能第二种方法更靠谱,复杂度为O(n)。因为如果按照第一种方式,每次都要拷贝父作用域的作用域链很麻烦,而且耗费内存,复杂度估计O(2n)。

作用域链的用途是什么呢,用来遍历查找标识符,闭包就是利用这点实现的。
查找一个标识符的顺序是:从作用域链的顶端,也就是自身的活动对象开始,如果存在则返回,如果不存在将继续搜索父函数的活动对象,依次查找,直到找到为止。如果整个作用域链上都无法找到(到全局对象结束),则返回undefined。

注意事项:

  • js中的函数运行在他们被定义的作用域,而不是被执行时候的作用域
  • 从作用域链的结构可以看出,在执行上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。因为全局变量总是存在于执行上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。
  • 在代码执行过程中,作用域链可以通过使用with语句和catch从句对象来扩充(它们的对象参数将会被作为活动对象加入到作用域链顶端)。并且由于这些对象是简单的对象,它们可以拥有原型(和原型链)。这个事实导致作用域链查找变为两个维度
    1. 先查找作用域链上的当前节点对象(活动)
    2. 然后查找该对象的原型链(如果有),一直查找到原型链顶端,遍历作用域链,重复1,2步

原型和原型链

首先需要搞清楚的问题是:当我们谈论原型时,我们到底在谈什么?

  1. 每个函数都有一个prototype属性,它指向一个对象,该对象就是在当该函数用作构造函数创建对象实例时,作为其对象实例的原型。通过该函数构造的所有对象共享该原型中的方法和属性。函数是一个对象,它有自己的原型但不是prototype
  2. 每个对象都有一个原型,根据浏览器不同一般表示为[[Prototype]]或者__proto__(好像已被最新标准实现),这才是我们真正谈论的原型,它从哪里来呢,就是我们在第一点提到的构造函数的那个prototype属性。

那么原型链是什么,用来干什么?
所有对象都有一个原型,而每个原型又有自己的一个原型,就样形成了原型链,原型链的顶端是[[object prototype]],该原型对象的的原型为null,就是原型链的终点。
当查找一个对象的属性时,js会从对象自己开始,然后从对象原型开始向上遍历原型链,直到找到指定属性为止,但如果直到原型链终点都仍然没有找到指定的属性,就会返回undefined。
下面看一张图总结原型原型链,是鸟哥文章中提到的一张图,被我改进了一下,不过相交的地方不太好,有空重构一下。
prototype

注意事项
如果在对象实例中重写了原型链上的方法或属性,那么该实例中的方法或属性会屏蔽原型链中的同名方法或属性,如果想要访问原型链上的同名方法或属性,只能通过delete删除在该实例上定义的同名方法或属性。

最后,我总结了一条关于原型原型链和作用域作用域链的明显区别:
原型原型链是用于在对象中查找属性和方法等标识符,作用域作用域链是用于查找变量和函数等标识符。


若有错误,望不吝指正。
参考资料请见上一篇学习摘录。

avatar

神无

舍悟离迷,六尘不改。