阅读 7635

一次搞定闭包和this

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

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

执行上下文

执行上下文是什么

可以简单理解执行上下文是js代码执行的环境,他的组成如下


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

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

五个关键点:

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

创建阶段

  1. 初始化作用域链
  2. 创建变量对象
    1. 创建arguments
    2. 扫描函数声明
    3. 扫描变量声明
  3. 求this

执行阶段

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

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属性。
函数代码分成两个阶段执行

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

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

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

举个例子


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
}

  1. 执行代码时

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

作用域链

先了解一下作用域

作用域

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

全局作用域:

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

  1. 在最外层定义的变量;

  2. 全局对象的属性

  3. 任何地方隐式定义的变量(未定义直接赋值的变量),在任何地方隐式定义的变量都会定义在全局作用域中,即不通过 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'复制代码
闭包的过程

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

  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 = 0
	function b() {
		console.log(x++)
	}
	function c() {
		console.log(x++)
	}
	
	return {
		b,
		c
	}
}
var r =  a()
r.b() // 0
r.c() // 1复制代码

最后

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

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

参考

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