本文是 「2019年,看了这一份, 不再怕前端面试了」中的一部分:javascript
参考了以前写过的博客
和额外的资料
, 分享给你们, 但愿能给你们带来一些启发和帮助
。前端
如需转载,请联系做者得到许可。java
上下文
是Javascript 中的一个比较重要的概念, 可能不少朋友对这个概念并非很熟悉, 那换成「做用域
」 和 「闭包
」呢?是否是就很亲切了。面试
「做用域」
和「闭包」
都是和「执行上下文」
密切相关的两个概念。segmentfault
在解释「执行上下文」是什么以前, 咱们仍是先回顾下「做用域」 和 「闭包」。微信
首先, 什么是做用域呢?闭包
域, 便是范围
。app
做用域,其实就是某个变量或者函数的可访问范围
。函数
它控制着变量和函数的可见性
和生命周期
。性能
做用域也分为: 「全局做用域
」和 「局部做用域
」。
若是一个对象在任何位置都能被访问到, 那么这个对象, 就是一个全局对象, 拥有一个全局做用域。
拥有全局做用域的对象能够分为如下几种状况:
JavaScript的做用域是经过函数
来定义的。
在一个函数中定义的变量, 只对此函数内部可见
。
这类做用域,称为局部做用域。
还有一个概念和做用域联系密切, 那就是做用域链
。
做用域链是一个集合
, 包含了一系列的对象, 它能够用来检索
上下文中出现的各种标识符
(变量, 参数, 函数声明等)。
函数在定义的时候, 会把父级的变量对象AO/VO的集合保存在内部属性 [[scope]]
中,该集合称为做用域链。
Javascript 采用了词法做用域
(静态做用域),函数运行在他们被定义
的做用域中,而不是他们被执行
的做用域。
看个简单的例子 :
var a = 3; function foo () { console.log(a) } function bar () { var a = 6 foo() } bar()
若是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是函数自身内部的。
与AO, VO 密切相关的概念还有GO, EC , 感兴趣的朋友能够参考:
https://blog.nixiaolei.com/20...
下面咱们说一下闭包。
闭包也是面试中常常会问到的问题, 考察的形式也很灵活, 譬如:
那闭包到底是什么呢?
说白了, 闭包其实也就是函数
, 一个能够访问自由变量
的函数。
自由变量
: 不在函数内部声明的变量。
不少所谓的代码规范里都说, 不要滥用闭包, 会致使性能问题, 我固然是不太认同这种说法的, 不过这个说法被人提出来,也是有一些缘由的。
毕竟,闭包里的自由变量会绑定
在代码块上,在离开创造它的环境下依旧生效,而使用代码块的人可能没法察觉。
闭包里的自由变量的形式有不少,先举个简单例子。
function add(p1){ return function(p2){ return p1 + p2; } } var a = add(1); var b = add(2); a(1) //2 b(1) // 3
在上面的例子里,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) }
使用当即执行函数
将 i 传入函数内部。
这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可使用外部函数的变量 j ,从而达到目的。
let
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
const , let 的原理和相关细节能够参考个人另外一篇:
解释完这两个概念,就回到咱们的主题, 上下文
。
首先, 执行上下文是什么呢?
简单来讲, 执行上下文就是Javascript 的执行环境
。
当javascript执行一段可执行
代码的时候时,会建立
对应的执行上下文
。
组成以下:
executionContextObj = { this, VO, scopeChain: 做用域链,跟闭包相关 }
因为Javavscript是单线程
的,一次只能处理一件事情,其余任务会放在指定上下文栈
中排队。
Javascript 解释器在初始化执行代码时,会建立一个全局执行上下文到栈中,接着随着每次函数的调用都会建立并压入一个新的执行上下文栈
。
函数执行后,该执行上下文被弹出。
执行上下文创建的步骤:
this 是Javascript中一个很重要的概念, 也是不少初级开发者容易搞混到的一个概念。
今天咱们就好好说道说道。
首先, this 是运行时
才能确认的, 而非定义时
确认的。
在函数执行时,this 老是指向调用该函数
的对象。
要判断 this 的指向,其实就是判断 this 所在的函数属于谁
。
this 的执行,会有不一样的指向状况, 大概能够分为:
咱们一个个来看。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 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都是指向window
的。
他们的操做会修改window中的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
结果证明了咱们上面的结论,并不存在什么优先。
var a = 4 function A() { this.a = 3 this.callA = function() { console.log(this.a) } } A() // 返回undefined, A().callA 会报错。callA被保存在window上 a = new A() a.callA() // 3, callA在 new A 返回的对象里
这个你们应该都很熟悉了。
令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
箭头函数比较特殊,它没有本身的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
以上咱们系统的介绍了上下文
, 以及与之相关的做用域
, 闭包
, this
等相关概念。
介绍了他们的做用,使用场景以及区别和联系。
但愿能对你们有所帮助, 文中如有纰漏, 欢迎指正, 谢谢。
若是以为内容有帮助能够关注下个人公众号 「 前端e进阶 」,了解最新动态。
也能够联系我加入微信群,群里有诸多大佬坐镇, 能够一块儿探讨技术, 一块儿摸鱼。一块儿学习成长!