1、函数的准备工作
函数在执行会进行一些准备工作,如创建一个“执行上下文”环境;执行上下文可以理解为当前代码的执行环境,它会形成一个作用域;
每个碰到可执行代码的时候都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况——全局代码,函数环境,eval代码。
- 全局环境:JavaScript代码运行起来会首先进入该环境
- 函数环境:当函数被调用执行时,会进入当前函数中执行代码
- eval
当代码在执行过程中,遇到以上三种情况,都会生成一个执行上下文,放入栈中,而处于栈顶的上下文执行完毕之后,就会自动出栈
因此在一个JavaScript程序中,必定会产生多个执行上下文,JavaScript引擎会以堆栈的方式来处理它们,这个堆栈,我们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。
执行上下文就在栈空间先进后出,后进先出;
如这段常见的代码:
var color = 'blue'; function changeColor() { var anotherColor = 'red'; function swapColors() { var tempColor = anotherColor; anotherColor = color; color = tempColor; } swapColors(); } changeColor();
(1)首先在栈底的是全局上下文,全局上下文入栈之后,其中的可执行代码开始执行,直到遇到了changeColor()
,这一句激活函数changeColor
创建它自己的执行上下文,然后就是changeColor的执行上下文入栈。
(2)changeColor的执行上下文入栈后,和上面进行相同的步骤,可执行代码执行,然后碰到了swapColor(),此时swapColor创建它自己的执行上下文,入栈。
(3)这时候在swapColors的可执行代码中,再没有遇到其他能生
成执行上下文的情况(没有碰到可执行的代码),因此这段代码顺利执行完毕,swapColors的上下文从栈中弹出。
(4)同理changColor也是一样,执行后弹出,最后就只剩下了全局上下文了(全局上下文在浏览器窗口关闭后出栈。)(注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。)
2、执行上下文是什么
关于执行上下文是什么,查了下资料,有这两个说法:
(1)
- 变量、函数表达式——变量声明,默认赋值为undefined;
- this——赋值;
- 函数声明——赋值;
这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
(2)
执行上下文可以分为两个阶段。
-
创建阶段
在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向 -
代码执行阶段
创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码
其实两个都是同样的东西,只不过第二种说法把上下文“拆解了”,对于我们初学者来说,这个更好的理解上下文究竟是什么。
那么就用第二种说法继续进行记录了;
3、变量对象(关于函数声明,变量提升)
console.log(a);//undenfiend
var a = 2;
一开始在学习JS时候,认为代码的执行顺序应该是从上到下的,因此这段代码的输出应该是会出错可是,实际情况输出的是undefined;这是为什么?这就要从执行上下文中的变量对象创建说起了;
我们上面说到执行上下文可以分为两个阶段,在创建阶段中,变量对象创建经历这个阶段:
-
建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
-
检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
-
检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
(注意下:这里是声明,相关变量函数还没赋值,赋值是在执行阶段中进行的);
这里看出相关的定义声明是在函数创建时候就进行了,而赋值此时还停留在“原地”,等待声明完成,因此上面的代码实际是这样运作的;
var a; console.log(a);//undenfiend a = 2;
变量a的位置被“移动”到了最上面,这个就是"提升”;
同时这里也有一点要注意的,是先检查函数声明,在检查变量声明的,也就是说函数是首先会被“提升”,然后才到变量的。
function test() { console.log(a); console.log(foo()); var a = 1; function foo() { return 2; } } test();
分析下test此时的变量对象(创建阶段) 应是这样的: argument:{...}, foo:{...(这里是foo函数的引用)},a:undefined,
当声明过后,进入了执行阶段,test此时的变量对象(执行阶段) 应是这样的:argument:{...},foo:{...},a:1;
因此实际的函数运作应是这样的:
function test() { function foo() { return 2; } var a; console.log(a); console.log(foo()); a = 1; } test();
注意的是:
未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。
问到变量对象和活动对象有什么区别,他们其实都是同一个对象,只是处于执行上下文的不同生命周期。
4、闭包
一、在学习闭包前,要了解两个概念:作用域和作用域链;
作用域:
在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
- 这里的标识符,指的是变量名或者函数名
JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。
作用域与执行上下文是完全不同的两个概念。我知道很多人会混淆他们,但是一定要仔细区分。
- JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

除了全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时确定。;
作用域链就是是这套规则的具体实现;
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
形象一点来说就好像“糖葫芦”那样,相关变量就是“糖葫芦”,作用域链就是木签;
作用域链自下而上地将所有嵌套定义的上下文所绑定的变量对象串接到一起,使嵌套的function可以“继承”上层上下文的变量,而并列的function之间互不干扰:
作用域链的最前端,始终都是当前执行代码所在的作用域的变量对象。
来看下代码:
var name = "test1"; function doSomething(){ var anotherName = "test2"; function showName(){ var author ="test3"; console(name) console(anotherName) // 在这里可以访问到 name 、anotherName 、author } showName(); // 在这里可以访问到 name anotherName ,不能访问到 author } doSomething(); // 在这里只能访问到 name
简单来说:只能从“里”查询到“外”进行查询;而不能从“外”到“里”;
在 JavaScript 中,每个函数都有着自己的作用域,在每次调用一个函数的时候 ,就会进入一个函数内的作用域,而当函数执行返回以后,就返回调用前的作用域。
一定要记住的一点是:JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里;
var name = 'test1'; function showName() { console.log(name); } function show() { var name = 'test2'; showName(); } show();//test1
同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。
二、闭包
上面做了这么多铺垫,为了就是认识和学习闭包这一重要的概念;
先直截了当的抛出闭包的定义:当函数可以记住并访问所在的作用域(全局作用域除外)时,就产生了闭包,即使函数是在当前作用域之外执行。
简单来说,假设函数A在函数B的内部进行定义了,并且当函数A在执行时,访问了函数B内部的变量对象,那么B就是一个闭包。
每个函数在调用时会创建新的上下文及作用域链,而作用域链就是将外层(上层)上下文所绑定的变量对象逐一串连起来,使当前函数可以获取外层上下文的变量、数据等。如果我们在函数中定义新的函数,同时将内层函数作为值返回,那么内层函数所包含的作用域链
将会一起返回,即使内层函数在其他上下文中执行,其内部的作用域链仍然保持着原有的数据,
而当前的上下文可能无法获取原先外层函数中的数据,使得函数内部的作用域链被保护起来,从而形成“闭包”。
所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量;
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(a); } fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn } function bar() { fn(); // 此处的保留的innerFoo的引用 } foo(); bar(); // 2
在上面的例子中,foo()
执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo
,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值;
有几点要注意的是:
-
闭包是在函数被调用执行的时候才被确认创建的。
-
闭包的形成,与作用域链的访问顺序有直接关系。
-
只有内部函数访问了上层作用域链中的变量对象时,才会形成闭包,因此,我们可以利用闭包来访问函数内部的变量。
(关于闭包相关概念还需要多多学习了解,这里只是作为学习笔记进行记录)
5、this
在上面有提到,this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境;在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。
先记录下结论:
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
从结论中我们可以看出,想要准确确定this指向,找到函数的调用者以及区分他是否是独立调用就变得十分关键。就是说this指向什么完全取决于函数在哪里被调用
在“你不知道的JS中”说:this的绑定规则有四种;
1、默认绑定
全局环境中的this,指向它本身。因此,这也相对简单,没有那么多复杂的情况需要考虑。
var a = 2; function foo(){ console.log(this.a) } foo()//2
2、隐式绑定(隐式赋值)
来看例子
var a = 20; var foo = { a: 10, getA: function () { return this.a; } } console.log(foo.getA()); // 10 var test = foo.getA; console.log(test()); // 20
foo.getA()
中,getA是调用者,他不是独立调用,被对象foo所拥有,因此它的this指向了foo。而test()
作为调用者,尽管他与foo.getA的引用相同,但是它是独立调用的,因此this指向undefined,在非严格模式,自动转向全局window。
再来一个例子:
function foo() { console.log(this.a) } function bar(fn) { fn(); // 真实调用者,为独立调用 } var a = 20; var obj = { a: 10, getA: foo } bar(obj.getA);
在这个例子里,obj.getA作为参数传进了bar中,此时可以看作fn=obj.getA;然后下面fn()
因此这时候也就和上面的一样,是看作全局调用了。
3、显式绑定(call、apply)
JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有着两个方法。它们除了参数略有不同,其功能完全一样。它们的第一个参数都为this将要指向的对象。
如下例子所示。fn并非属于对象obj的方法,但是通过call,我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了。这就是call/apply的用法。
function fn() { console.log(this.a); } var obj = { a: 2 } fn.call(obj);
这里的话,原本运行fn,此时的this是window,但用call把作用域强制绑定在obj上,因此最后输出的是2;
拓展下:在es5中增加了bind方法;apply,call,bind的说明如下
- apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
- apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
- apply 、 call 、bind 三者都可以利用后续参数传参;
- bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
function fn() { console.log(this.a); } var obj = { a: 2 } fn.call(obj);//2 function fn() { console.log(this.a); } var obj = { a: 2 } fn.bind(obj)();//2
bind返回的是一个函数,需要在后面添加括号才执行,当我们需要回调执行的时候,使用 bind() 方法;
在ES6中还增加了箭头函数的写法,使用箭头函数,此时箭头函数则会捕获其所在上下文的 this
值,作为自己的 this
值
var a=1; var obj={ a:2, fn:function(){ console.log(this.a); return function(){ console.log(this.a) } } } obj.fn()()//2,1 var a=1; var obj={ a:2, fn:function(){ console.log(this.a); return ()=>{ console.log(this.a) } } } obj.fn()()//2,2
obj.fn()()这是的调用者如上文说的,其实是window,因为this在被隐式绑定的时候丢失了this的上下文绑定对象,从而把this绑定到全局,之前的话一般用一个变量 var that = this;
来存储this,而在es6可以用使用箭头函数,直接绑定上下文的this;
当然这里只是对箭头函数的一些初步认识和了解而已;
四、new绑定(构造函数与原型方法上的this)
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getName = function() { return this.name; } var p1 = new Person('Nick', 20); p1.getName();
我们已经知道,this,是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。
通过new操作符调用构造函数,会经历以下4个阶段。
- 创建一个新的对象;
- 将构造函数的this指向这个新对象;
- 指向构造函数的代码,为这个对象添加属性,方法等;
- 返回新对象。
因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对象返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象,p1。
而原型方法上的this就好理解多了,根据上边对函数中this的定义,p1.getName()
中的getName为调用者,他被p1所拥有,因此getName中的this,也是指向了p1。
参考资料:http://www.jianshu.com/u/10ae59f49b13
http://www.cnblogs.com/wangfupeng1988/p/3977924.html
http://blog.rainy.im/2015/07/04/scope-chain-and-prototype-chain-in-js/
https://bonsaiden.github.io/JavaScript-Garden/zh/#function.closures
http://web.jobbole.com/83642/
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions
《你不知道的js》-闭包和this部分
这篇文章纯粹是自己学习的记录笔记而已,里面的内容来源均有说明来源,十分感谢他们的分享。