闭包和this,是两个相当高频的考点,然而你有没有想过,实际上他们两个都跟同一个知识点相关?

有请我们的这篇文章的主角,执行上下文

执行上下文

执行上下文是什么

可以简单理解执行上下文是js代码执行的环境,当js执行一段可执行代码时,会创建对应的执行上下文。他的组成如下


executionContextObj = {this: 对的就是你关注的那个this,VO:变量对象,scopeChain: 作用域链,跟闭包相关
}

由于JS是单线程的,一次只能发生一件事情,其他事情会放在指定上下文栈中排队。js解释器在初始化执行代码时,会创建一个全局执行上下文到栈中,接着随着每次函数的调用都会创建并压入一个新的执行上下文栈。函数执行后,该执行上下文被弹出。

五个关键点:

  1. 单线程
  2. 同步执行
  3. 一个全局上下文
  4. 无限制函数上下文
  5. 每次函数调用创建新的上下文,包括调用自己
执行上下文建立的步奏

创建阶段

  1. 初始化作用域链
  2. 创建变量对象

    1. 创建arguments
    2. 扫描函数声明
    3. 扫描变量声明
  3. 求this

执行阶段

  1. 初始化变量和函数的引用
  2. 执行代码

<!-- more -->

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 = fooa = 3
bar() // 3不是2

通过这个例子可以更加了解this是函数调用时才确定的

再绕一点


function foo() {console.log( this.a );
}function doFoo(fn) {this.a = 4fn();
}var obj = {a: 2,foo: foo
};var a =3doFoo( obj.foo ); // 4


function foo() {this.a = 1console.log( this.a );
}function doFoo(fn) {this.a = 4fn();
}var obj = {a: 2,foo: foo
};var a =3doFoo( 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 = 4fn();
}var obj = {a: 2,foo: foo
};var a =3doFoo( obj.foo ); // 4
setTimeout(obj.foo,0) // 1

上面的代码结果可以证实我们的猜测。

用new构造就指向新对象

a = 4
function A() {this.a = 3this.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

大家应该都很熟悉,令this指向传递的第一个参数,如果第一个参数为null,undefined或是不传,则指向全局变量


a = 3
function foo() {console.log( this.a );
}var obj = {a: 2
};foo.call( obj ); // 2foo.call( null ); // 3foo.call( undefined ); // 3foo.call(  ); // 3var 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); //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指向obj2}
}
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.a) // 1
函数执行上下文的变量对象

函数上下文中,变量对象VO就是活动对象AO。

初始化时,带有arguments属性。
函数代码分成两个阶段执行

  1. 进入执行上下文时
    此时变量对象包括

    1. 形参
    2. 函数声明,会替换已有变量对象
    3. 变量声明,不会替换形参和函数
  2. 函数执行

根据代码修改变量对象的值

举个例子


function test (a,c) {console.log(a, b, c, d) // 5 undefined [Function: c] undefinedvar b = 3;a = 4function c () {}var d = function () {}console.log(a, b, c, d) // 4 3 [Function: c] [Function: d]var c = 5console.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

}

  1. 执行代码时

通过最后的console可以发现,函数声明可以被覆盖

作用域链

先了解一下作用域

作用域

变量与函数的可访问范围,控制着变量及函数的可见性与生命周期。分为全局作用域和局部作用域。

全局作用域:

在代码中任何地方都能访问到的对象拥有全局作用域,有以下几种:

  1. 在最外层定义的变量;
  2. 全局对象的属性
  3. 任何地方隐式定义的变量(未定义直接赋值的变量),在任何地方隐式定义的变量都会定义在全局作用域中,即不通过 var 声明直接赋值的变量。

局部作用域:

JavaScript的作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,称为函数(局部)作用域

作用域链

作用域链是一个对象列表,用以检索上下文代码中出现的标识符。
标识符可以理解为变量名称,参数,函数声明。

函数在定义的时候会把父级的变量对象AO/VO的集合保存在内部属性[[scope]]中,该集合称为作用域链。
自由变量指的是不在函数内部声明的变量。
当函数需要访问自由变量时,会顺着作用域链来查找数据。子对象会一级一级的向上查找父对象的变量,父对象的变量对子对象是可见的,反之不成立。
作用域链就是在所有内部环境中查找变量的链式表。

可以直接的说,JS采用了词法作用域(静态作用域),JS的函数运行在他们被定义的作用域中,而不是他们被执行的作用域。可以举一个例子说明:

var s = 3
function a () {console.log(s)
}function b () {var s = 6a()
}b() // 3,不是6

如果js采用动态作用域,打印出来的应该是6而不是3,这个例子说明了js是静态作用域。

函数作用域链的伪代码:

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 = 1function b() {console.log(num++)}return b
}var c1 = a()
c1() // '1'
c1() // '2'var c2 = a()
c2() // '1'
c2() // '2'
闭包的过程

写的不是很严谨。可能省略了一些过程

  1. 运行函数a

    1. 创建函数a的VO,包括变量num和函数b
    2. 定义函数b的时候,会保存a的变量对象VO和全局变量对象到[[scope]]中
    3. 返回函数b,保存到c1
  2. 运行c1

    1. 创建c1的作用域链,该作用域链保存了a的变量对象VO
    2. 创建c1的VO
    3. 运行c1,这是发现需要访问变量num,在当前VO中不存在,于是通过作用域链进行访问,找到了保存在a的VO中的num,对它进行操作,num的值被设置成2
  3. 再次运行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 = 0function b() {console.log(x++)}function c() {console.log(x++)}return {b,c}
}var r =  a()
r.b() // 0
r.c() // 1

补充一个查看作用域链和闭包的技巧
打开chrome控制台

console.dir(r.b)f b() {[[Scopes]]: [{x:0}, {type: 'global', name: '', object: Window}]
}

最后

最后,我们再来总结一下执行上下文的过程,加深下印象


var scope = "global scope";
function checkscope(a){var scope2 = 'local scope';
}checkscope(5);
创建全局上下文执行栈

创建全局变量globalContext.VO.

创建checkscope函数

将全局变量VO保存为作用域链,设置到函数的内部属性[[scope]]

checkscope.[[scope]] = [globalContext.VO
];
执行checkscope函数

创建函数执行上下文,将checkscope函数执行上下文压入执行上下文栈

ECStack = [checkscopeContext,globalContext
];
函数执行上下文创建阶段

第一步是复制[[scope]],创建作用域链


checkscopeContext = {Scope: checkscope.[[scope]],
}

第二步是创建活动对象AO

checkscopeContext = {AO: {arguments: {0: 5length: 1},a: 5scope2: undefined},Scope: checkscope.[[scope]],
}

第三步是将活动对象AO放入作用域链顶端


checkscopeContext = {AO: {arguments: {0: 5length: 1},a: 5scope2: undefined},Scope:  [AO, checkscope.[[scope]]],
}

第四步,求出this,上下文创建阶段结束

这里的this等于window

进入函数执行阶段

随着函数执行,修改AO的值

    AO: {arguments: {0: 5length: 1},a: 5scope2: 'local scope'},
函数执行完毕

函数上下文从执行上下文栈弹出

ECStack = [globalContext
];

文章写的比较长,涉及的范围也比较广,可能有不少的错误,希望大家可以指正。

本文章为前端进阶系列的一部分,
欢迎关注和star本博客或是关注我的github

参考

  1. 深入理解ES6箭头函数中的this
  2. 你不知道的JS上卷
  3. JavaScript深入之执行上下文栈
  4. 理解JavaScript的作用域链
  5. JavaScript深入之变量对象
  6. 深入理解JavaScript系列(12):变量对象(Variable Object)
  7. 了解JavaScript的执行上下文

一次搞定this和闭包相关推荐

  1. 轻松搞定javascript变量(闭包,预解析机制,变量在内存的分配 )

    变量:  存储数据的容器 1.声明        var 2.作用域全局变量. 局部变量. 闭包(相对的全局变量): 3.类型a.基本类型(undefined, null, boolean, numb ...

  2. [译] 12步轻松搞定python装饰器 - 简书

    [译] 12步轻松搞定python装饰器 - 简书 呵呵!作为一名教python的老师,我发现学生们基本上一开始很难搞定python的装饰器,也许因为装饰器确实很难懂.搞定装饰器需要你了解一些函数式编 ...

  3. 破解前端面试系列(3):如何搞定纸上代码环节?

    很多重视技术的互联网公司在工程师招聘的技术面环节都会要求候选人在纸上写代码(后文用"纸上代码"代称),面试官想通过这种方式考察哪些点?候选人该注意哪些点?本文基于美团早几年常用的一 ...

  4. (一)梳理前端知识体系,搞定大厂必考面试题

    梳理前端知识体系,搞定大厂必考面试题 常见面试题 JS基础知识 变量类型和计算 原型和原型链 作用域和闭包 异步和单线程 运行环境 HTTP协议 总结 常见面试题 JS基础知识 变量类型和计算 typ ...

  5. Python高级特性: 12步轻松搞定Python装饰器

    12步轻松搞定Python装饰器 通过 Python 装饰器实现DRY(不重复代码)原则:  http://python.jobbole.com/84151/ 基本上一开始很难搞定python的装饰器 ...

  6. 12步轻松搞定python装饰器

    http://python.jobbole.com/81683/ 呵呵!作为一名教python的老师,我发现学生们基本上一开始很难搞定python的装饰器,也许因为装饰器确实很难懂.搞定装饰器需要你了 ...

  7. python中装饰器修复技术_12步轻松搞定Python装饰器

    作为一名教python的老师,我发现学生们基本上一开始很难搞定python的装饰器,也许因为装饰器确实很难懂.搞定装饰器需要你了解一些函数式编程的概念,当然还有理解在python中定义和调用函数相关语 ...

  8. python 找到装饰器_[译] 12步轻松搞定python装饰器

    呵呵!作为一名教python的老师,我发现学生们基本上一开始很难搞定python的装饰器,也许因为装饰器确实很难懂.搞定装饰器需要你了解一些函数式编程的概念,当然还有理解在python中定义和调用函数 ...

  9. 12步轻松搞定 Python 装饰器

    12步轻松搞定 Python 装饰器 1. 函数 在python中,函数通过def关键字.函数名和可选的参数列表定义.通过return关键字返回值.我们举例来说明如何定义和调用一个简单的函数: > ...

  10. 快速搞定前端技术一面 匹配大厂面试要求学习笔记

    快速搞定前端技术一面 匹配大厂面试要求学习笔记 第1章 课程介绍[说说面试的那些事儿] 本章会出几个面试题,分析每道题目设计的知识点,然后总结出一个完整的知识体系.让我们开始 "题目-> ...

最新文章

  1. 更改tomcat的request编码方式
  2. Spark GraphX
  3. bzoj 3218: a + b Problem
  4. centos重新安装yum
  5. C++字符读入函数(getchgetchar)
  6. 相分离和长链非编码RNA之间的故事Paraspeckles: Where Long Noncoding RNA Meets Phase Separation
  7. Android Studio稀奇古怪的疑难杂症
  8. 第25章 串行FLASH文件系统FatFs—零死角玩转STM32-F429系列
  9. 07-android-基站定位
  10. Shader实现透明反射效果应用地板
  11. STM32开发实例 基于STM32单片机的智能快递系统
  12. 大数据有哪些存储方式?
  13. LSV加载大面积实景三维模型出现偏移,如何投影变换处理?
  14. KDGX-A光缆故障断点检测仪
  15. excel单元格一分为二还要输入文字,不能编辑是什么原因?
  16. 通达云OA2015版及钉钉、微信办公集成产品正式发布
  17. @PersistenceContext和@Autowired在EntityManager上应用的区别。
  18. APP被网信办点名下架还能重新上架吗?恢复上架流程请收好
  19. GDOI2017旅游记
  20. 云钻还在吗 苏宁怎么解除实名认证_实名认证-苏宁如何修改实名认证我想修改实名认证信?苏 – 手机爱问...

热门文章

  1. 如何测试java定时器_Java--定时器测试程序
  2. mod java 求余_java中求余%与取模floorMod的区别
  3. 万兆网口和千兆网口区别_万兆网卡的安装以及注意事项
  4. php_eol为什么没有换行,PHP PHP_EOL 换行符
  5. 532. 数组中的K-diff数对
  6. Oracle 10g 数据库连接出现The Network Adapter could not establish the connection解决办法
  7. Torch环境搭建遇到的问题
  8. 【从线性回归到BP神经网络】第一部分:协方差与相关系数
  9. HDU 2144(最长公共子序列+并查集)
  10. Raki的读paper小记:Style Transformer