最近在看面试题,也参加了两次面试,很多都在问关于JavaScript的基础知识和语言特性,所以在这篇里集中总结一下关于js本身的原型链、闭包、作用域(链)的知识基础,希望被问到的时候能答得上来。

原型链

原型

js是基于对象的语言,原型是对象的基础。一个对象通过内部属性绑定到它的原型,在一些浏览器如火狐,Safari,Chrome中可以使用__proto__来访问对象的原型。当创建一个内置类型的实例时,这些实例自动拥有一个Object作为它们的原型。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

经常被问的问题是关于__proto__prototype, 要搞明白这二者关系,一个隐含条件是在函数的基础上讨论;而根据犀牛书的定义,除了字符串、数字、true、false、null和undefined之外,JavaScript的值都是对象,所以函数也属于对象,是一种函数对象。prototype是为其他对象提供共享属性的对象,但这个对象,在对象实例中其实是个隐藏属性,js的早起没有标准的API可以直接访问,如果我没记错,应该是火狐首先在浏览器中实现了__proto__属性,后来各大浏览器也分别实现了__proto__, 由于这个api已经为事实标准,因此在ES6中被加入了语言标准,浏览器的__proto__实际访问的是Object.prototype.proto。所以__proto__属性用来读取或设置当前对象的prototype对象。

创建对象

在对象被创建时,对象的__proto__属性指向其构造函数的prototype属性,而如果一个函数没有指定prototype属性,那么这个属性将默认为一个空的Object,而Object.prototype.__proto__null

> function A(){}
> new A().__proto__ === A.prototype
true
> new A().constructor === A.prototype.constructor
true
> A.prototype.__proto__
Object {}
> A.prototype.__proto__.__proto__
null

原型链

将一个构造好的对象传递给另一个需要构造的对象的构造函数的prototype属性,这样新构造函数所构造的对象的__proto__自然就指向之前构造好的对象了。这就是原型链。查找一个对象的属性的时候,如果遍历对象当前的所有属性,如果不存在,就会顺着原型链一层一层遍历原型链上的对象,直到查找成功。

继承

js有多达8种继承方式,各种乱七八糟的继承操作,其中最重要也是最本质的是原型链继承,js中的继承就是使用这种原型继承方式来实现的。

> function A(){};
> function B(){};
> B.prototype = new A();
> var b = new B();
> b instanceof A
true
> b instanceof B
true
> b instanceof Object
true

我们可以用instanceof运算符来判断一个对象是那种对象的实例,当B继承了A,无论是检测B还是A还是Object,都应该返回true。

instanceof

object instanceof constructor的实际行为是检测constructor.prototype是否存在于object的原型链上

constructor属性

对象实例中的constructor属性,指向其构造函数。

image.png

constructor这个属性是在创建构造函数时赋值到构造函数的prototype属性中的。而这时,这个constructor指向这个构造函数本身。

imaged62174887a4a7745.png

而上面我们说了,在使用构造函数创建实例的时候,会将构造函数的prototype属性传递给实例的__proto__属性,所以实例的constructor属性就能指向自己的构造函数了。

其他的坑

一些坑,比如Array.prototype其实是个数组实例:

> Array.prototype
[]

作用域链

每一个JavaScript函数都会被表示为一个函数对象,它和其他对象一样,拥有自己的属性,其中有一些是JavaScript引擎所使用的内部属性,不允许外部访问,[[scope]]就是其中一个。

scope属性

内部[[scope]]属性包含一个函数被创建的作用域对象的集合,被称为作用域链。它决定函数可以访问哪些属性。作用域链中的每个对象都被称为一个VO(Variable Object),每个VO都以“键值对”的形式存在。当一个函数创建以后,它的作用域链就会被填充,这些对象代表创建此函数的环境中可访问的数据。

创建运行期上下文

在函数运行时,会创建一个内部对象,称为运行期上下文(Execution Context,也叫EC),这个运行期上下文在创建后回访制在“运行期栈”中,和其他语言的函数调用的栈类似。一个运行期上下文定义了一个函数运行时的环境。函数的每次运行都会创建一个运行期上下文,每次创建的运行期上下文相互独立。当函数执行完毕时,运行期上下文就会被销毁

复制作用域链

一个运行期上下文有自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被初始化,并将所运行函数的[[scope]]中的VO对象按照顺序复制到运行期上下文自己的作用域链中

AO对象的创建

在复制完函数的作用域链之后,就会创建一个被称为AO(Activation Object)的对象,这个对象包含了以下内容:

  1. this
  2. 局部变量
  3. 命名参数
  4. arguments集合

创建完成AO对象后,它江北推入作用域链的最前端。作用域链被销毁时,AO也会一并销毁

标示符解析

函数运行过程中,每遇到一个变量,标识符都需要决定需要从哪里获得数据。这个搜索过程会顺着运行期上下文的作用域链来查找。大致上流程如下:

  1. 索引一个AO或VO
  2. 查找其中的局部变量
  3. 查找其中的命名参数
  4. 如果没找到,查找下一个AO或VO,如果有则转到1,否则转到5
  5. 输出标识符未定义

with和try-catch改变作用域链

一般来说,运行期上下文的作用域链是不会被改变的。但是使用with和try-catch可以对作用域链进行临时改变。

with所做的就是在作用域链前端再插入一个VO,with所指定的对象的所有属性,都会被插入到这个VO中。由此,访问with所指定的对象的属性速度将会加快,但由于增长了作用域链的长度,访问其他属性将会减慢。所以使用with不如直接使用一个变量来暂存with所指定的对象

try-catch中的catch块也有相同的效果,在catch块中,会将异常对象构造成一个VO,插入到运行时上下文的作用域链的最前端。同样会加长作用域链,导致性能下降。解决办法就是在catch块中调用一个处理函数,所有的逻辑放在处理函数之中。

闭包

闭包的强大在于它允许函数访问局部范围之外的数据。当一个函数被执行时,会创建一个AO,而如果在函数执行时创建了闭包,当前运行期上下文的作用域链将复制到闭包的作用域链中

由于闭包的作用域链与运行期上下文作用域链中的引用相同,这也就是说在函数的AO与运行期上下文一同销毁时,由于闭包的存在,AO中的对象依旧早闭包的作用域链中被引用,导致JavaScript的垃圾回收器认为AO中的对象已然是活对象,而不会对其进行垃圾回收。所以闭包将会导致更多的内存开销

柯里

柯里是对闭包的一个典型应用。

类型

类型在也会经常被问到,在具体编码中我们也会经常使用类型的判断。js一共有8种类型,其中7中原始类1种对象类,没有什么特别好说的,可以在MDN上自行查看细节。其中的原始类是不可变类型,因为js是种动态语言,编码时我们操作最多的是那种动态变量,变量是不管类型的,所以我们对不可变的感触会不如写抢类型语言的同学那么深刻。这个特性在用js刷LeetCode时会遇到一些坑。

类型判断

这里要区分typeofinstanceof这两个运算符。instanceof在上文已经讲过,其实是在比较instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,这也是MDN的标准定义。而typeof则用来比较数据类型,但它无法判断由object派生的高级对象,比如Array、Date等

而唯一能精准判断Array的是通过对象的toString打印:

>var kkk = [];
>Object.prototype.toString.call(kkk) === "[object Array]";
true

线程与异步

大多数浏览器只有一个单独的处理线程,它由两个任务共享:JavaScript任务和用户界面更新任务,每个时刻只有一个操作得以执行。当JavaScript代码执行时,用户界面就会被“锁定”,反过来也是一样。他们所共享的这个线程就被称为UI线程。

比如一次按钮点击,会依次将按钮样式改变、按钮点击时需要运行的JavaScript代码、代码中的UI改变依次加入到UI线程中。

浏览器的限制

对于js脚本,浏览器除了会限制运行期栈的深度以外,还会限制长时间执行脚本。不同的浏览器判断脚本执行时间过长的方法不一样,有的是通过时间来判断,有的是通过运行语句的条数判断。

而对于异步资源的加载,大部分浏览器在同域下最高允许6个并发异步请求,所以有人会考虑将资源放在不同域名下来提高并发数量,加快页面打开速度(域名分片),或者支持http2来突破上限。

eventloop

setTimeout、setInterval、setImmediate、nextTick