正文
上下文 是 JavaScript 中的一个比较重要的概念, 可能很多朋友对这个概念并不是很熟悉, 那换成「作用域」 和 「闭包」呢? 是不是就很亲切了.
「作用域」和「闭包」 都是和「执行上下文」密切相关的两个概念.
在解释「执行上下文」是什么之前, 我们还是先回顾下「作用域」 和 「闭包」.
作用域
首先, 什么是作用域呢?
域, 即是范围.
作用域, 其实就是某个变量或者函数的可访问范围.
它控制着变量和函数的可见性和生命周期.
作用域也分为: 「全局作用域 」和 「局部作用域」.
全局作用域:
如果一个对象在任何位置都能被访问到, 那么这个对象, 就是一个全局对象, 拥有一个全局作用域.
拥有全局作用域的对象可以分为以下几种情况:
定义在最外层的变量
全局对象的属性
任何地方隐式定义的变量(即: 未定义就直接赋值的变量). 隐式定义的变量都会定义在全局作用域中.
局部作用域:
JavaScript 的作用域是通过函数来定义的.
在一个函数中定义的变量, 只对此函数内部可见.
这类作用域, 称为局部作用域.
还有一个概念和作用域联系密切, 那就是作用域链.
作用域链
作用域链是一个集合, 包含了一系列的对象, 它可以用来检索上下文中出现的各类标识符(变量, 参数, 函数声明等).
函数在定义的时候, 会把父级的变量对象 AO/VO 的集合保存在内部属性 [[scope]] 中, 该集合称为作用域链.
AO : Activation Object 活动对象
VO : Variable object 变量对象
JavaScript 采用了词法作用域(静态作用域), 函数运行在他们被定义的作用域中, 而不是他们被执行的作用域.
看个简单的例子 :
- var a = 3;
- function foo () {
- console.log(a)
- }
- function bar () {
- var a = 6
- foo()
- }
- bar()
web 前端开发学习 Q-q-u-n: 731771211, 分享学习的方法和需要注意的小细节, 不停更新最新的教程和学习方法(详细的前端项目实战教学视频, PDF)
如果 JS 采用动态作用域, 打印出来的应该是 6 而不是 3.
这个例子说明了 javasript 是静态作用域.
此函数作用域链的伪代码:
- function bar() {
- function foo() {
- // ...
- }
- }
- bar.[[scope]] = [
- globalContext.VO
- ];
- foo.[[scope]] = [
- barContext.AO,
- globalContext.VO
- ];
函数在运行激活的时候, 会先复制 [[scope]] 属性创建作用域链, 然后创建变量对象 VO, 然后将其加入到作用域链.
- executionContextObj: {
- VO: {},
- scopeChain: [VO, [[scope]]]
- }
总的来说, VO 要比 AO 的范围大很多, VO 是负责把各个调用的函数串联起来的.
VO 是外部的, 而 AO 是函数自身内部的.
下面我们说一下闭包.
闭包
闭包也是面试中经常会问到的问题, 考察的形式也很灵活, 譬如:
描述下什么是闭包
写一段闭包的代码
闭包有什么用
给你一个闭包的例子, 让你修改, 或者看输出
那闭包究竟是什么呢?
说白了, 闭包其实也就是函数, 一个可以访问自由变量的函数.
自由变量: 不在函数内部声明的变量.
很多所谓的代码规范里都说, 不要滥用闭包, 会导致性能问题, 我当然是不太认同这种说法的, 不过这个说法被人提出来, 也是有一些原因的.
毕竟, 闭包里的自由变量会绑定在代码块上, 在离开创造它的环境下依旧生效, 而使用代码块的人可能无法察觉.
闭包里的自由变量的形式有很多, 先举个简单例子.
- function add(p1){
- return function(p2){
- return p1 + p2;
- }
- }
- var a = add(1);
- var b = add(2);
- a(1) //2
- b(1) // 3
Web 前端开发学习 Q-q-u-n: 731771211, 分享学习的方法和需要注意的小细节, 不停更新最新的教程和学习方法(详细的前端项目实战教学视频, PDF)
在上面的例子里, a 和 b 这两个函数, 代码块是相同的, 但若是执行 a(1)和 b(1)的结果却是不同的, 原因在于这两者所绑定的自由变量是不同的, 这里的自由变量其实就是函数体里的 p1 .
自由变量的引入, 可以起到和 OOP 里的封装同样作用, 我们可以在一层函数里封装一些不被外界知晓的自由变量, 从而达到相同的效果, 很多模块的封装, 也是利用了这个特性.
然后说一下我遇到的真实案例, 是去年面试腾讯 QQ 音乐的一道笔试题:
- for (var i = 1; i <= 5; i++) {
- setTimeout(function timer() {
- console.log(i)
- }, i * 1000)
- }
这段代码会输出一堆 6, 让你改一下, 输出 1, 2, 3, 4, 5
解决办法还是很多的, 就简单说两个常见的.
用闭包解决
- for (var i = 1; i <= 5; i++) {
- ;(function(j) {
- setTimeout(function timer() {
- console.log(j)
- }, j * 1000)
- })(i)
- }
Web 前端开发学习 Q-q-u-n: 731771211, 分享学习的方法和需要注意的小细节, 不停更新最新的教程和学习方法(详细的前端项目实战教学视频, PDF)
使用立即执行函数将 i 传入函数内部.
这个时候值就被固定在了参数 j 上面不会改变, 当下次执行 timer 这个闭包的时候, 就可以使用外部函数的变量 j , 从而达到目的.
[推荐] 使用 let
- for (let i = 1; i <= 5; i++) {
- setTimeout(function timer() {
- console.log(i)
- }, i * 1000)
- }
执行上下文
首先, 执行上下文是什么呢?
简单来说, 执行上下文就是 JavaScript 的执行环境.
当 JavaScript 执行一段可执行代码的时候时, 会创建对应的执行上下文.
组成如下:
- executionContextObj = {
- this,
- VO,
scopeChain: 作用域链, 跟闭包相关
}
由于 Javavscript 是单线程的, 一次只能处理一件事情, 其他任务会放在指定上下文栈中排队.
JavaScript 解释器在初始化执行代码时, 会创建一个全局执行上下文到栈中, 接着随着每次函数的调用都会创建并压入一个新的执行上下文栈.
函数执行后, 该执行上下文被弹出.
执行上下文建立的步骤:
创建阶段
初始化作用域链
创建变量对象
创建 arguments
扫描函数声明
扫描变量声明
求 this
执行阶段
初始化变量和函数的引用
执行代码
this
this 是 JavaScript 中一个很重要的概念, 也是很多初级开发者容易搞混到的一个概念.
今天我们就好好说道说道.
首先, this 是运行时才能确认的, 而非定义时确认的.
在函数执行时, this 总是指向调用该函数的对象.
要判断 this 的指向, 其实就是判断 this 所在的函数属于谁.
this 的执行, 会有不同的指向情况, 大概可以分为:
指向调用对象
指向全局对象
用 new 构造就指向新对象
apply/call/bind, 箭头函数
我们一个个来看.
1. 指向调用对象
- function foo() {
- console.log( this.a );
- }
- var obj = {
- a: 2,
- foo: foo
- };
- obj.foo(); // 2
2. 指向全局对象
这种情况最容易考到, 也最容易迷惑人.
先看个简单的例子:
- var a = 2;
- function foo() {
- console.log( this.a );
- }
- foo(); // 2
没什么疑问.
看个稍微复杂点的:
- function foo() {
- console.log( this.a );
- }
- function doFoo(fn) {
- this.a = 4
- fn();
- }
- var obj = {
- a: 2,
- foo: foo
- };
- var a = 3
- doFoo( obj.foo ); // 4
对比:
- function foo() {
- this.a = 1
- console.log( this.a );
- }
- function doFoo(fn) {
- this.a = 4
- fn();
- }
- var obj = {
- a: 2,
- foo: foo
- };
- var a = 3
- doFoo(obj.foo); // 1
发现不同了吗?
你可能会问, 为什么下面的 a 不是 doFoo 的 a 呢?
难道是 foo 里面的 a 被优先读取了吗?
打印 foo 和 doFoo 的 this, 就可以知道, 他们的 this 都是指向 Windows 的.
他们的操作会修改 Windows 中的 a 的值. 并不是优先读取 foo 中设置的 a.
简单验证一下:
- function foo() {
- setTimeout(() => this.a = 1, 0)
- console.log( this.a );
- }
- function doFoo(fn) {
- this.a = 4
- fn();
- }
- var obj = {
- a: 2,
- foo: foo
- };
- var a = 3
- doFoo(obj.foo); // 4
- setTimeout(obj.foo, 0) // 1
Web 前端开发学习 Q-q-u-n: 731771211, 分享学习的方法和需要注意的小细节, 不停更新最新的教程和学习方法(详细的前端项目实战教学视频, PDF)
结果证实了我们上面的结论, 并不存在什么优先.
3. 用 new 构造就指向新对象
- var a = 4
- function A() {
- this.a = 3
- this.callA = function() {
- console.log(this.a)
- }
- }
- A() // 返回 undefined, A().callA 会报错. callA 被保存在 Windows 上
- a = new A()
- a.callA() // 3, callA 在 new A 返回的对象里
- 4. apply/call/bind
这个大家应该都很熟悉了.
令 this 指向传递的第一个参数, 如果第一个参数为 null,undefined 或是不传, 则指向全局变量.
- var a = 3
- function foo() {
- console.log( this.a );
- }
- var obj = {
- a: 2
- };
- foo.call(obj); // 2
- foo.call(null); // 3
- foo.call(undefined); // 3
- foo.call(); // 3
- var obj2 = {
- a: 5,
- foo
- }
- obj2.foo.call() // 3, 不是 5
- //bind 返回一个新的函数
- function foo(something) {
- console.log(this.a, something);
- return this.a + something;
- }
- var obj =
- a: 2
- };
- var bar = foo.bind(obj);
- var b = bar(3); // 2 3
- console.log(b); // 5
5. 箭头函数
箭头函数比较特殊, 它没有自己的 this. 它使用封闭执行上下文 (函数或是 global) 的 this 值:
- var x=11;
- var obj={
- x:22,
- say: () => {
- console.log(this.x);
- }
- }
- obj.say(); // 11
- obj.say.call({x:13}) // 11
- x = 14
- obj.say() // 14
- // 对比一下
- var obj2={
- x:22,
- say() {
- console.log(this.x);
- }
- }
- obj2.say();// 22
- obj2.say.call({x:13}) // 13
Web 前端开发学习 Q-q-u-n: 731771211, 分享学习的方法和需要注意的小细节, 不停更新最新的教程和学习方法(详细的前端项目实战教学视频, PDF)
总结
以上我们系统的介绍了上下文, 以及与之相关的作用域, 闭包, this 等相关概念.
介绍了他们的作用, 使用场景以及区别和联系.
希望能对大家有所帮助, 文中若有纰漏, 欢迎指正, 谢谢.
来源: http://www.jianshu.com/p/3335d8b1eaff