闭包和this
执行上下文
执行上下文是什么
可以简单理解执行上下文是js代码执行的环境,当js执行一段可执行代码时,会创建对应的执行上下文。
由于JS是单线程的,一次只能发生一件事情,其他事情会放在指定上下文栈中排队。js解释器在初始化执行代码时,会创建一个全局执行上下文到栈中,接着随着每次函数的调用都会创建并压入一个新的执行上下文栈。函数执行后,该执行上下文被弹出。
五个关键点:
1.单线程
2.同步执行
3.一个全局上下文
4.无限制函数上下文
5.每次函数调用创建新的上下文,包括调用自己
执行上下文建立的步奏
创建阶段
1.初始化作用域链
2.创建变量对象
3.创建arguments
4.扫描函数声明
5.扫描变量声明
6.求this
执行阶段
1.初始化变量和函数的引用
2.执行代码
this
在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。如果是window调用this指向window
指向调用对象
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
我的理解是类式于作用域原理, 取最近的
因此如果把代码改成
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
大家应该都很熟悉,令this指向传递的第一个参数,如果第一个参数为null,undefined或是不传,则指向全局变量
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
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就是全局对象。
ar a = 1
console.log(window.a) // 1
console.log(this.a) // 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]]中,该集合称为作用域链。
自由变量指的是不在函数内部声明的变量。
当函数需要访问自由变量时,会顺着作用域链来查找数据。子对象会一级一级的向上查找父对象的变量,父对象的变量对子对象是可见的,反之不成立。
作用域链就是在所有内部环境中查找变量的链式表。
可以直接的说,JS采用了词法作用域(静态作用域),JS的函数运行在他们被定义的作用域中,而不是他们被执行的作用域。可以举一个例子说明:
var s = 3
function a () {
console.log(s)
}
function b () {
var s = 6
a()
}
b() // 3,不是6
如果js采用动态作用域,打印出来的应该是6而不是3,这个例子说明了js是静态作用域。
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'
通过上面的运行结果,我们可以观察到,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
补充一个查看作用域链和闭包的技巧
打开chrome控制台
console.dir(r.b)
f b() {
[[Scopes]]: [
{x:0},
{type: 'global', name: '', object: Window}
]
}