闭包和 this, 是两个相当高频的考点, 然而你有没有想过, 实际上他们两个都跟同一个知识点相关?
有请我们的这篇文章的主角, 执行上下文
执行上下文
执行上下文是什么
可以简单理解执行上下文是 js 代码执行的环境, 他的组成如下
executionContextObj = {
this: 对的就是你关注的那个 this,
VO: 变量对象,
scopeChain: 作用域链, 跟闭包相关
}
由于 JS 是单线程的, 一次只能发生一件事情, 其他事情会放在指定上下文栈中排队 js 解释器在初始化执行代码时, 会创建一个全局执行上下文到栈中, 接着随着每次函数的调用都会创建并压入一个新的执行上下文栈函数执行后, 该执行上下文被弹出
五个关键点:
单线程
同步执行
一个全局上下文
无限制函数上下文
每次函数调用创建新的上下文, 包括调用自己
执行上下文建立的步奏
创建阶段
初始化作用域链
创建变量对象
创建 arguments
扫描函数声明
扫描变量声明
求 this
执行阶段
初始化变量和函数的引用
执行代码
this
在函数执行时, this 总是指向调用该函数的对象要判断 this 的指向, 其实就是判断 this 所在的函数属于谁
指向调用对象
- function foo() {console.log( this.a);
- }
- var obj = {
- a: 2,
- foo: foo
- };
- obj.foo(); // 2
指向全局对象
- function foo() {
- console.log( this.a );
- }
- var a = 2;
- foo(); // 2
注意
- // 接上
- var bar = foo
- a = 3
- bar() // 3 不是 2
通过这个例子可以更加了解 this 是函数调用时才确定的
再绕一点
- 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
这是为什么呢? 是因为优先读取 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
上面的代码结果可以证实我们的猜测
用 new 构造就指向新对象
- a = 4
- function A() {
- this.a = 3
- this.callA = function() {
- console.log(this.a)
- }
- }
- A() // 返回 undefined, A().callA 会报错 callA 被保存在 window 上
- var a = new A()
- a.callA() // 3,callA 在 new A 返回的对象里
- apply/call/bind
- function foo() {
- console.log( this.a );
- }
- var obj = {
- a: 2
- };
- foo.call( obj ); // 2
- //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); //this 指向 window
- }
- }
- obj.say();// 11
- obj.say.call({x:13}) // 11
- x = 14
- obj.say() // 14
- // 对比一下
- var obj2={
- x:22,
- say() {
- console.log(this.x); //this 指向 window
- }
- }
- obj2.say();// 22
- obj2.say.call({x:13}) // 13
事件监听函数
指向被绑定的 dom 元素
- document.body.addEventListener('click',function(){
- console.log(this)
- }
- )
- // 点击网页
- // <body>...</body>
- html
HTML 标签的属性中是可能写 JS 的, 这种情况下 this 指代该 HTML 元素
- <div id="foo" onclick="console.log(this);"></div>
- <script type="text/javascript">
- document.getElementById("foo").click(); //logs <div id="foo"...
- </script>
变量对象
变量对象是与执行上下文相关的数据作用域, 存储了上下文中定义的变量和函数声明
变量对象式一个抽象的概念, 在不同的上下文中, 表示不同的对象
全局执行上下文的变量对象
全局执行上下文中, 变量对象就是全局对象
在顶层 js 代码中, this 指向全局对象, 全局变量会作为该对象的属性来被查询在浏览器中, window 就是全局对象
- var a = 1
- console.log(window.a) // 1
- console.log(this.1) // 1
函数执行上下文的变量对象
函数上下文中, 变量对象 VO 就是活动对象 AO
初始化时, 带有 arguments 属性
函数代码分成两个阶段执行
进入执行上下文时
此时变量对象包括
形参
函数声明, 会替换已有变量对象
变量声明, 不会替换形参和函数
函数执行
根据代码修改变量对象的值
举个例子
- function test (a,c) {
- console.log(a, b, c, d) // 5 undefined [Function: c] undefined
- var b = 3;
- a = 4
- function c () {
- }
- var d = function () {
- }
- console.log(a, b, c, d) // 4 3 [Function: c] [Function: d]
- var c = 5
- console.log(a, b, c, d) // 4 3 5 [Function: d]
- }
- test(5,6)
来分析一下过程
1. 创建执行上下文时
- VO = {
- arguments: {0:5},
- a: 5,
- b: undefined,
- c: [Function], // 函数 C 覆盖了参数 c, 但是变量声明 c 无法覆盖函数 c 的声明
- d: undefined, // 函数表达式没有提升, 在执行到对应语句之前为 undefined
- }
执行代码时
通过最后的 console 可以发现, 函数声明可以被覆盖
作用域链
先了解一下作用域
作用域
变量与函数的可访问范围, 控制着变量及函数的可见性与生命周期分为全局作用域和局部作用域
全局作用域:
在代码中任何地方都能访问到的对象拥有全局作用域, 有以下几种:
在最外层定义的变量;
全局对象的属性
任何地方隐式定义的变量(未定义直接赋值的变量), 在任何地方隐式定义的变量都会定义在全局作用域中, 即不通过 var 声明直接赋值的变量
局部作用域:
JavaScript 的作用域是通过函数来定义的, 在一个函数中定义的变量只对这个函数内部可见, 称为函数 (局部) 作用域
作用域链
作用域链是一个对象列表, 用以检索上下文代码中出现的标识符
标识符可以理解为变量名称, 参数, 函数声明
函数在定义的时候会把父级的变量对象 AO/VO 的集合保存在内部属性 [[scope]] 中, 该集合称为作用域链
自由变量指的是不在函数内部声明的变量
当函数需要访问自由变量时, 会顺着作用域链来查找数据
- function foo() {
- function bar() {
- ...
- }
- }
- foo.[[scope]] = [
- globalContext.VO
- ];
- bar.[[scope]] = [
- fooContext.AO,
- globalContext.VO
- ];
函数在运行激活的时候, 会先复制 [[scope]] 属性创建作用域链, 然后创建变量对象 VO, 然后将其加入到作用域链
- executionContextObj: {
- VO:{},
- scopeChain: [VO, [[scope]]]
- }
闭包
闭包是什么
闭包按照 mdn 的定义是可以访问自由变量的函数自由变量前面提到过, 指的是不在函数内部声明的变量
闭包的形式
- function a() {
- var num = 1
- function b() {
- console.log(num++)
- }
- return b
- }
- var c1 = a()
- c1() // '1'
- c1() // '2'
- var c2 = a()
- c2() // '1'
- c2() // '2'
闭包的过程
写的不是很严谨可能省略了一些过程
运行函数 a
创建函数 a 的 VO, 包括变量 num 和函数 b
定义函数 b 的时候, 会保存 a 的变量对象 VO 和全局变量对象到 [[scope]] 中
返回函数 b, 保存到 c1
运行 c1
创建 c1 的作用域链, 该作用域链保存了 a 的变量对象 VO
创建 c1 的 VO
运行 c1, 这是发现需要访问变量 num, 在当前 VO 中不存在, 于是通过作用域链进行访问, 找到了保存在 a 的 VO 中的 num, 对它进行操作, num 的值被设置成 2
再次运行 c1, 重复第二步的操作, num 的值设置成 3
一些问题
通过上面的运行结果, 我们可以观察到, c2 所访问 num 变量跟 c1 访问的 num 变量不是同一个变量我们可以修改一下代码, 来确认自己的猜想
- function a() {
- var x = {
- y: 4
- }
- function b() {
- return x
- }
- return b
- }
- var c1 = a() var c2 = a() c1 === c2() // false
因此我们可以确定, 闭包所访问的变量, 是每次运行父函数都重新创建, 互相独立的
注意, 同一个函数中创建的自由变量是可以在不同的闭包共享的
- function a() {
- var x = 0
- function b() {
- console.log(x++)
- }
- function c() {
- console.log(x++)
- }
- return {
- b,
- c
- }
- }
- var r = a()
- r.b() // 0
- r.c() // 1
最后
文章写的比较长, 涉及的范围也比较广, 可能有不少的错误, 希望大家可以指正
本文章为前端进阶系列 http://hpoenixf.com/前端进阶系列-目录.html 的一部分,
来源: https://juejin.im/entry/5ac21eb8f265da2392368433