毕业也整整一年了,看着不少学弟都毕业了,突然心中很有感慨,时间一去不复还呀。记得从去年这个时候接触到JavaScript,从一开始就很喜欢这门语言,当时迷迷糊糊看完了《JavaScript高级程序设计》这本书,似懂非懂。这几天又再次回顾了这本书,以前不少不理解的内容彷佛开始有些豁然开朗了。为了防止以后本身又开始模糊,因此本身来总结一下JavaScript中关于 做用域链和原型链的知识,并将两者相比较看待进一步加深理解。如下内容都纯属于本身的理解,有不对的地方欢迎指正。javascript
首先咱们须要了解的是做用域作什么的?当JavaScript引擎在某一做用域中碰见变量和函数的时候,须要可以明确变量和函数所对应的值是什么,因此就须要做用域来对变量和函数进行查找,而且还须要肯定当前代码是否对该变量具备访问权限。也就是说做用域主要有如下的任务:前端
举一个例子:java
function foo(a) {
console.log( a ); // 2
}
foo( 2 );复制代码
对于上述代码,JavaScript引擎须要对做用域发出如下的命令git
foo
,获得变量后执行该变量a
,获得变量后对其赋值为2console
,获得变量后准备执行属性log
a
,获得变量后,做为参数传入console.log
执行 咱们省略了函数console.log
内部的执行过程,咱们能够看到对JavaScript引擎来讲,做用域最重要的功能就是查询标识符。从上面的例子来看,引擎对变量的使用其实不是都同样的。好比第一步引擎获得标识符foo
的目的是执行它(或者说是为了拿到标识符里存储的值)。
但第二步中引擎查找标识符a
的目的是为了对其赋值(也就是改变存储的值)。因此查找也分为两种:LHS
和RHS
。github
我在以前的一篇文章中从LHS与RHS角度浅谈Js变量声明与赋值曾经介绍过LHS
与RHS
,这两个看起来很高大上的名词其实很是简单。LHS
指的是Left-hand Side
,而RHS
指的是Right-hand Side
。分别对应于两种不一样目的的词法查询。LHS
所查询的目的是为了赋值(相似于该变量会位于赋值符号=
的左边),例如第二步查找变量a
的过程。而RHS
所查询的目的是为了引用(相似于变量会位于赋值符号=
的右边),例如第一步查找变量foo
的过程。 面试
咱们知道代码不只仅能够访问当前的做用域的变量,对于嵌套的父级做用域中的变量也能够访问。咱们先只在ES5中表述,咱们知道JavaScript在ES5中是没有块级做用域的,只有函数能够建立做用域。举个例子: 闭包
function Outer(){
var outer = 'outer';
Inner();
function Inner(){
var inner = 'inner';
console.log(outer,inner) // outer inner
}
}复制代码
当引擎执行到函数Inner
内部的时候,不只能够访问当前做用域并且能够访问到Outer
的做用域,从而能够访问到标识符outer
。所以咱们发现当多个做用域相互嵌套的时候,就造成了做用域链。词法做用域在查找标识符的时候,优先在本做用域中查找。若是在本做用域没有找到标识符,会继续向上一级查找,当抵达最外层的全局做用域仍然没有找到,则会中止对标识符的搜索。若是没有查找到标识符,会根据不一样的查找方式做出不一样的反应。若是是RHS
,则会抛出Uncaught ReferenceError
的错误,若是是LHS
,则会在查找最外层的做用域声明该变量,这就解释了为何对未声明的变量赋值后该变量会成为全局变量。因此上面的代码执行ide
console.log(outer,inner)
函数
的时候,引擎会首先要求Inner
函数的词法做用域查找(RHS
)标识符outer
,被告知该词法做用域不存在该标识符,而后引擎会要求嵌套的上一级Outer
词法做用域查找(RHS
)标识符outer
,Outer
词法做用域的查找成功并将结果返回给引擎。ui
上面咱们理解做用域链都是从做用域链查找变量的角度去考虑的,其实这已经足够了,大部分做用域链的场景都是查找标识符。可是咱们能够换一个角度去理解做用域链。其实JavaScript的每一个函数都有对应的执行环境(execution context)。当执行流进入进入一个函数时,该函数的执行环境就会被推入环境栈,当函数执行结束以后,该函数的执行环境就会被弹出环境栈,执行环境被变动为以前的执行环境。而每建立一个执行环境时,会同时生成一个变量对象(variable object)(函数生成的是活动变量(activation object)),用来存储当前执行环境中定义的变量和函数,当执行环境结束时,当前的变量(活动)对象就会被销毁(全局的变量对象是一直存在的,不会被销毁)。虽然咱们没法访问到变量(活动)对象,但词法做用域查找标识符会使用它。
当对于函数的执行环境生成的活动对象,初始化就会存在两个变量:this
和arguments
,所以咱们在函数中就直接可使用这两个变量。对于做用域链存储都是变量(活动)对象,而当前执行环境的变量对象就存储在做用域链的最前端,优先被查找。从这个角度看,标识符解析是沿着做用域链一级一级地在变量(活动)对象中搜索标识符的过程。搜索过程始终从做用域链的前端开始,而后逐级地向后回溯,直至找到标识符为止。
这年头出去面试JavaScript的岗位,各个都要问你闭包的问题,开始的时候以为闭包的概念蛮高级的,后来以为这个也没啥东西可讲的。老早的以前就写过一篇关于闭包的文章浅谈JavaScript闭包,讲到如今我以为把闭包放到做用域链一块儿将会更好。仍是继续讲个例子:
function fn(){
var a = 'JavaScript';
function func(){
console.log(a);
}
return func;
}
var func = fn();
func(); //JavaScript复制代码
首先明确一下什么是闭包?我认为闭包最好的概念解释就是:
函数在定义的词法做用域之外的地方被调用,闭包使得函数能够继续访问定义时的词法做用域。
func
函数执行的位置和定义的位置是不相同的,func
是在函数fn
中定义的,但执行倒是在全局环境中,虽然是在全局函数中执行的,但函数仍然能够访问当定义时的词法做用域。以下图所示:
咱们以前说过,当函数执行结束后其活动变量就会被销毁,可是在上面的例子中却不是这个样子。但函数fn
执行结束以后,fn
对象的活动变量并无被销毁,这是由于fn
返回的函数func
的做用域链还保持着fn
的活动变量,所以JavaScript的垃圾回收机制不会回收fn
活动变量。虽然返回的函数func
是在全局环境下执行的,可是其做用域链的存储的活动(变量)对象的顺序分别是:func
的活动变量、fn
的活动变量、全局变量对象。所以在func
函数执行时,会顺着做用域链查找标识符,也就能访问到fn
所定义的词法做用域(即fn
函数的活动变量)也就不足为奇了。这样看起来是否是以为闭包也是很是的简单。
说完了做用域链,咱们来说讲原型链。首先也是要明确什么是原型?全部的函数都有一个特殊的属性: prototype
(原型),prototype
属性是一个指针,指向的是一个对象(原型对象),原型对象中的方法和属性均可以被函数的实例所共享。所谓的函数实例是指以函数做为构造函数建立的对象,这些对象实例均可以共享构造函数的原型的方法。举个例子:
var Person = function(name){
this.name = name;
}
Person.prototype.sayName = function(){
console.log('name: ', this.name)
};
var person = new Person('JavaScript');
person.sayName(); //JavaScript复制代码
在上面的例子中,对象person
是构造函数Person
建立的实例。所谓的构造函数也只不过是普通的函数经过操做符new
来调用。在使用new
操做符调用函数时主要执行如下几个步骤:
经过构造函数返回的对象,其中含有一个内部指针[[Prototype]]
指向构造函数的原型对象,固然咱们是没法访问到这个标准的内部指针[[Prototype]]
,可是在Firefox、Safari和Chrome在上都支持一个属性__proto__
,用来指向构造函数的原型对象。下图就解释了上面的结构:
咱们能够看到,构造函数Person
的prototype
属性指向Prototype
的原型对象。而person
做为构造函数Person
建立的实例,其中存在内部指针也指向Person
的原型对象。须要注意的是,在Person
的原型对象中存在一个特殊的属性constructor
,指向构造函数Person
。在咱们的例子中,执行到:
person.sayName(); //JavaScript复制代码
当执行person
的sayName
属性时,首先会在对象实例中查找sayName
属性,当发现对象实例中不存在sayName
时,会转而去搜索person
内部指针[[Prototpe]]
所指向的原型对象,当发现原型对象中存在sayName
属性时,执行该属性。关于函数sayName
中this
的指向,有兴趣能够戳这篇文章一个小小的JavaScript题目。
讲完了原型,再讲讲原型链,其实咱们上面的图并不完整,由于全部函数的默认原型都是Object的实例,因此函数原型实例的内部指针[[Prototype]]
指向的是Object.prototype
,让咱们继续来完善一下:
person.toString()复制代码
执行上面代码时,首先会在对象实例person
中查找属性toString
方法,咱们发现实例中不存在toString
属性。而后咱们转到person
内部指针[[Prototype]]
指向的Person
原型对象去寻找toString
属性,结果是仍然不存在。这找不到咱们就放弃了?开玩笑,咱们这么有毅力。咱们会再接着到Person
原型对象的内部指针[[Prototype]]
指向的Object
原型对象中查找,此次咱们发现其中确实存在toString
属性,而后咱们执行toString
方法。发现了没有,这一连串的原型造成了一条链,这就是原型链。
其实咱们上面例子中对属性toString
查找属于RHS
,以RHS
方式寻找属性时,会在原型链中依次查找,若是在当前的原型中已经查找到所须要的属性,那么就会中止搜索,不然会一直向后查找原型链,直到原型链的结尾(这一点有点相似于做用域链),若是直到原型链结尾仍未找到,那么该属性就是undefined
。但执行LHS
方式的查找却大相径庭,当发现对象实例自己不存在该属性,直接在该对象实例中声明变量,而不会去查找原型链。例如:
person.toString = function(){
console.log('person')
}
person.toString(); //person复制代码
当对person
执行LHS
的方式查找toString
属性时,咱们发现person
中并不存在toString
,这时会直接在person
中声明属性,而不会去查找原型链,接着咱们执行person.toString()
时,咱们在实例中找到了toString
属性并将其执行,这样实例中的toString
就屏蔽了原型链中的toString
属性。
讲完了做用域链和原型链,咱们能够比较一下。做用域链的做用主要用于查找标识符,看成用域须要查询变量的时候会沿着做用域链依次查找,若是找到标识符就会中止搜索,不然将会沿着做用域链依次向后查找,直到做用域链的结尾。而原型链是用于查找引用类型的属性,查找属性会沿着原型链依次进行,若是找到该属性会中止搜索并作相应的操做,不然将会沿着原型链依次查找直到结尾。
若是以为阅读完了本篇文章对你有些许帮助,欢迎你们我关注个人掘金帐号或者star个人Github的blog项目,也算是对个人鼓励啦!