前言:JS 的作用域、执行上下文、this、闭包是老生常谈的话题,也是新手比较懵懂的知识点。当然即便你作为老手,也未必真的能理解透彻这些概念。
一、作用域和执行上下文
作用域:
js中的作用域是词法作用域,即由 函数声明时 所在的位置决定的。词法作用域是指在编译阶段就产生的,一整套函数标识符的访问规则。(区别于词法作用域,动态作用域是在函数执行的时候确认的,js的没有动态作用域,但js的this
很像动态作用域,后面会提到。词法作用域的概念十分重要,请多加记忆并理解。) 说到底js的作用域只是一个“空地盘”,其中并没有真实的变量,但是却定义了变量如何访问的规则。
作用域链本质上是一个指向变量对象的指针列表,它只引用不包含实际变量对象,是作用域概念的延申。作用域链定义了变量在当前上下文访问不到的时候如何沿作用域链继续查询的一套规则。
执行上下文:
执行上下文是指 函数调用时 产生的变量对象,这个变量对象我们无法直接访问,但是可以访问其中的变量、this
对象等。例如:
let fn, bar; // 1、进入全局上下文环境
bar = function(x) {
let b = 5;
fn(x + b); // 3、进入fn函数上下文环境
};
fn = function(y) {
let c = 5;
console.log(y + c); //4、fn出栈,bar出栈
};
bar(10); // 2、进入bar函数上下文环境
每次函数调用时,都会产生一个新的执行上下文环境,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前处于活动状态的正在执行的上下文,也称为活动对象(running execution context,图中蓝色的块),区别与底下被挂起的上下文(变量对象)。
总结:作用域是在函数声明的时候就确定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的环境。也就是说作用域定义了执行上下文中的变量的访问规则,执行上下文在这个作用域规则的前提下进行变量查找,函数引用等具体操作。
总结:作用域是在函数声明的时候就确定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的环境。也就是说作用域定义了执行上下文中的变量的访问规则,执行上下文在这个作用域规则的前提下进行变量查找,函数引用等具体操作。
理解函数的执行过程
函数的执行过程分成两部分,一部分用来生成执行上下文环境,确定this的指向、声明变量以及生成作用域链;另一部分则是按顺序逐行执行代码。
建立执行上下文阶段:(发生在 函数被调用时 && 函数体内的代码执行前 )
- 生成变量对象,顺序:创建 arguments 对象 --> 创建function函数声明 --> 创建var变量声明
- 生成作用域链
- 确定this的指向
函数执行阶段:
- 逐行执行代码,这个阶段会完成变量赋值,函数引用,以及执行其他代码。
二、this指向
关于js的this
关键字,我记得第一次接触还是在做前端半年或一年的时候(哈哈我就是这么水)。那时候徐哥(java大佬)教我在绑定click
事件的时候把this
传给事件处理函数,类似<button onclick="handle(this)">确认</button>
,我当时就懵了,this
是什么鬼?!从此正式开启了我三年的js痛苦之旅:封装啊、闭包啊、面向对象啊、继承啊等等等等。this
的指向说来说去其实只有四种:
let fn = function(){
alert(this.name)
}
let obj = {
name: '',
fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
- 方法1中直接调用函数
fn()
,这种看着像光杆司令的调用方式,this
指向window
(严格模式下是undefined
)。 - 方法2中是点调用
obj.fn()
,此时this
指向obj
对象。点调用中this
指的是点前面的对象。 - 方法3中利用
call
函数把fn
中的this
指向了第一个参数,这里是obj
。即利用call
、apply
、bind
函数可以把函数的this
变量指向第一个参数。 - 方法4中用
new
实例化了一个对象instance
,这时fn
中的this
就指向了实例instance
。
如果同时发生了多个规则怎么办?其实上面四条规则的优先级是递增的:
fn() < obj.fn() < fn.call(obj) < new fn()
首先,new
调用的优先级最高,只要有new
关键字,this
就指向实例本身;接下来如果没有new
关键字,有call、apply、bind
函数,那么this
就指向第一个参数;然后如果没有new、call、apply、bind
,只有obj.foo()
这种点调用方式,this
指向点前面的对象;最后是光杆司令foo()
这种调用方式,this
指向window
(严格模式下是undefined
)。
es6中新增了箭头函数,而箭头函数最大的特色就是没有自己的this、arguments、super、new.target
,并且箭头函数没有原型对象prototype
不能用作构造函数(new
一个箭头函数会报错)。因为没有自己的this
,所以箭头函数中的this
其实指的是包含函数中的this
。无论是点调用,还是call
调用,都无法改变箭头函数中的this
。
三、闭包
js的闭包是新手的噩梦,在学js的前三年,我查阅了无数的博文,苦苦搜索闭包的概念,然而最终一无所获。MDN上这样定义闭包:闭包是函数和声明该函数的词法环境的组合。
what?能说人话吗?
很长时间以来我对闭包都停留在“定义在一个函数内部的函数”这样肤浅的理解上。事实上这只是闭包形成的必要条件之一。直到后来看了kyle大佬的《你不知道的JAVASCRIPT》上册中关于闭包的定义,我才豁然开朗:
当函数能够记住并访问所在的词法作用域时,就产生了闭包。
let single = (function(){
let count = 0
return {
plus(){
count++
return count
},
minus(){
count--
return count
}
}
})()
single.plus() //
single.minus() //
这是个单例模式,这个模式返回了一个对象并赋值给变量single
,变量single
中包含两个函数plus
和minus
,而这两个函数都用到了所在词法作用域中的变量count
。正常情况下count
和所在的执行上下文会在函数执行结束时被销毁,但是由于count
还在被外部环境使用,所以在函数执行结束时count
和所在的执行上下文不会被销毁,这就产生了闭包。每次调用single.plus()
或者single.minus()
,就会对闭包中的count
变量进行修改,这两个函数就保持住了对所在的词法作用域的引用。
闭包其实是一种特殊的函数,它可以访问函数内部的变量,还可以让这些变量的值始终保持在内存中,不会在函数调用后被垃圾回收机制清除。
看个经典案例:
// 方法1
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
方法1中,循环设置了五个定时器,一秒后定时器中回调函数将执行,打印变量i
的值。毋庸置疑,一秒之后i
已经递增到了5,所以定时器打印了五次5 。(定时器中并没有找到当前作用域的变量i
,所以沿作用域链找到了全局作用域中的i
)
方法2中,由于es6的let
会创建局部作用域,所以循环设置了五个作用域,而五个作用域中的变量i
分布是1-5,每个作用域中又设置了一个定时器,打印一秒后变量i
的值。一秒后,定时器从各自父作用域中分别找到的变量i
是1-5 。这是个利用闭包解决循环中变量发生异常的新方法。
最后
我真的学不动了。
链接:https://juejin.im/post/5cf8612df265da1bcb4f1bf8
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。