目录
JavaScript闭包和this
1 闭包
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
在JS中,通俗来讲,闭包就是能够读取外层函数内部变量的函数
1.1 变量作用域
变量的作用域为两种:全局作用域和局部作用域
1)函数内部可以读取全局变量
let code = 200;
function f1() {
console.log(code);
}
f1(); // 200
2)函数外部无法读取函数内部的局部变量
function f1() {
let code = 200;
}
console.log(code); // Uncaught ReferenceError: code is not defined
1.2 读取函数内部的局部变量
1)在函数内部再定义一个函数
function f1() {
let code = 200;
function f2() {
console.log(code);
}
}
函数f1内部的函数f2可以读取f1中所有的局部变量。因此,若想在外部访问函数f1中的局部变量code
,可通过函数f2间接访问
2)为外部程序提供访问函数局部变量的入口
function f1() {
let code = 200;
function f2() {
console.log(code);
}
return f2;
}
f1()(); // 200
1.3 闭包概念
1.2中的函数f2,就是闭包,其作用就是将函数内部与函数外部进行连接
-
闭包访问的变量,是每次运行上层函数时重新创建的,是相互独立的
function f1() {
let obj = {};
function f2() {
return obj;
}
return f2;
}
let result1 = f1();
let result2 = f1();
console.log(result1() === result2()); // false
-
不同的闭包,可以共享上层函数中的局部变量
function f() {
let num = 0;
function f1() {
console.log(++num);
}
function f2() {
console.log(++num);
}
return {f1,
f2};
}
let result = f();
result.f1(); // 1
result.f2(); // 2
从结果可以看出,闭包f1和闭包f2共享上层函数中的局部变量num。
旅行者走路的问题
function factory() {
var start = 0
function walk(step) {
var new_total = start + step
start = new_total
return start
}
return walk
}
var res = factory()
console.info(res(1))
console.info(res(2))
console.info(res(3))
start会记录上次一的值
使用闭包的注意点:
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
2 this
2.1 关键点:
-
this始终指向调用该函数的对象
-
若没有指明调用的对象,则顺着作用域链向上查找,最顶层为global(window)对象
-
箭头函数中的this是定义函数时绑定的,与执行上下文有关
-
简单对象(非函数、非类)没有执行上下文;
-
类中的this,始终指向该实例对象;
-
箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。
2.2 四类调用方式
1)作为对象方法的调用
function f() {
console.log( this.code );
}
let obj = {
code: 200,
f: f
};
obj.f(); // 200
2)纯粹的函数调用
function f() {
console.log( this.code );
}
// 此处,通过var(函数作用域)声明的变量code会绑定到window上;如果使用let(块作用域)声明变量code,则不会绑定到window上,因此下面的2次函数调用f(),会输出undefined
// let code = 200;
var code = 200;
f(); // 200
code = 404;
f(); // 404
注意:
通过var(函数作用域)声明的变量code会绑定到window上;如果使用let(块作用域)声明变量code,则不会绑定到window上
复杂一点:
function doF(fn) {
this.code = 404;
fn();
}
function f() {
console.log(this.code);
}
let obj = {
code: 200,
f: f
};
var code = 500;
doF(obj.f); // 404
该列子中,为分析出this的指向,应找到关键点,哪个对象调用了函数f()。obj.f作为doF()的入参,将函数f传给了doF,而doF是由window对象调用的,所以函数doF中的this指向window,继而函数f中的this也指向window。
但最终执行是doF所以this指向doF,结果404
function doF(fn) {
this.code = 404;
fn();
}
function f() {
this.code = 401;
console.log(this.code);
}
let obj = {
code: 200,
f: f
};
var code = 500;
doF(obj.f); // 401
3)作为[构造函数]调用
code = 404
function A() {
this.code = 200
this.callA = function() {
console.log(this.code)
}
}
var a = new A()
a.callA() // 200, callA在new A返回的对象里
4)使用apply、call、bind调用
apply
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数
var code = 404;
let obj = {
code: 200,
f: function() {
console.log(this.code);
}
}
obj.f(); // 200, 实际上是作为对象的方法调用
obj.f.apply(); // 404,参数为空时,默认使用全局对象global,在此处为对象window
obj.f.apply(obj); //200,this指向参数中设置的对象
call
函数实例的call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数
function f() {
console.log( this.code );
}
var obj = {
code: 200
};
f.call( obj ); // 200
bind
bind()
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数
// bind返回一个新的函数
function f(b) {
console.log(this.a, b);
return this.a + b;
}
var obj = {
a: 2
};
var newF = f.bind(obj);
var result = newF(3); // 2 3
console.log(result); // 5
2.3 箭头函数中的this
箭头函数中的this是定义函数时绑定的,而不是在执行函数时绑定。若箭头函数在简单对象中,由于简单对象没有执行上下文,所以this指向上层的执行上下文;若箭头函数在函数、类等有执行上下文的环境中,则this指向当前函数、类。
1)箭头函数在普通对象中
var code = 404;
let obj = {
code: 200,
getCode: () => {
console.log(this.code);
}
}
obj.getCode(); // 404
在箭头函数中,this 的值是在定义函数时确定的,而不是在运行时确定的。在这个例子中,箭头函数 getCode 是在对象 obj 定义时创建的,而不是在调用 obj.getCode() 的时候
箭头函数中的 this 指向的是外层的语法作用域的 this 值,而不是指向调用它的对象。在全局作用域中,this 指向的是全局对象(在浏览器环境中通常是 window 对象)。所以,当箭头函数中使用 this.code 时,它实际上是引用全局作用域中的 code 变量,其值为 404
2)箭头函数在函数中
var code = 404;
function F() {
this.code = 200;
let getCode = () => {
console.log(this.code);
};
getCode();
}
var f = new F() //200
var f = F() //构造函数没有new调用,就成为了一个普通函数
console.log(f)
console.log(code)
3)箭头函数在类中
var code = 404;
class Status {
constructor(code) {
this.code = code;
}
getCode = () => {
console.log(this.code);
};
}
let status = new Status(200);
status.getCode(); // 200
注意:
不管是箭头函数还是普通函数,只要是类中,this就指向实例对象
3 样例详解
1)
var code = 404;
let status = {
code : 200,
getCode : function() {
return function(){
return this.code;
};
}
};
console.log(status.getCode()()); // 404
执行status.getCode()
时,返回函数,status.getCode()()
表示执行当前返回的函数,其调用者为全局变量window,所以this.code
为绑定在window中的code
,值为404。
2)
var code = 404;
let status = {
code : 200,
getCode : function() {
let that = this;
return function(){
return that.coe;
};
}
};
console.log(status.getCode()()); // 200
执行status.getCode()
时,this指向status,并通过局部变量that
保存this的值,最后返回值为函数。status.getCode()()
表示执行返回的函数,其that
指向的status,所以返回值为200。
3)更复杂的例子
function f() {
//宏任务
setTimeout(() => {
console.log(">>>" + this); // >>>[object object],语句5
this.code = 401;
}, 0)
//同步
console.log( this.code );
}
let obj = {
">>>" + this
code: 200,
foo: f
};
var code = 500;
同步和异步 关键词说的很好
宏任务 微任务
1.宏任务 微任务 同步异步
2.箭头函数this指向问题
3.字符串+ this [object object]
obj.foo(); // 200,语句1
console.log("--" + obj.code); // --200,语句3
//宏任务
setTimeout(()=>{console.log("---" + obj.code);}, 0); // ---401,语句4
obj.foo(); (语句1):调用 obj 对象的 foo 方法。
输出:200
解释:在 foo 方法内部的 console.log(this.code) 打印出 obj 对象的 code 属性,其值为 200。
console.log("--" + obj.code); (语句3):打印 obj 对象的 code 属性。
输出:--200
解释:在全局作用域中,code 被赋值为 500,但这里的 obj.code 指向的是 obj 对象的 code 属性,其值仍然是 200。
setTimeout(() => { console.log("---" + obj.code); }, 0); (语句4):设置一个零延迟的定时器,其中的箭头函数在调用。
输出:---401
解释:在调用 obj.foo() 的过程中,foo 方法中的 setTimeout 在当前宏任务结束后执行。由于是箭头函数,this 的值保持与父作用域一致(也就是 obj 对象)。所以在箭头函数内部,this.code 被设置为 401。
setTimeout(() => { console.log(">>>" + this); this.code = 401; }, 0) (语句5):设置一个零延迟的定时器,其中的箭头函数在调用。
输出:>>>[object Object]
解释:在调用 obj.foo() 的过程中,foo 方法中的 setTimeout 在当前宏任务结束后执行。箭头函数的 this 始终指向它被创建时的外部作用域,所以 this 指向了 obj 对象,而在控制台中打印 this 时会将其转换为字符串。所以输出为 >>>[object Object]。
知识补充 --- 函数setTimeout
用于创建一个定时器,同一个对象,各个定时器使用一个编号池,不同的对象使用独立的编号池,同一个对象上的多个定时器有不同的定时器编号。所以,setTimeout
到了执行时间点时,其内部的this指向定时器所绑定的对象
结果分析 --- 函数setTimeout中传入的函数句柄,由于js是单线程执行,即使延时为0,仍需等到本次执行的所有同步代码执行完毕,才能执行。所以在两次执行obj.foo()的过程中,其内部的setTimeout的入参函数(也就是语句5)都未执行。同理,执行语句3时,语句5同样未执行。直到执行语句4,当前同步代码块执行完毕,语句5执行(并且执行了2次,因为语句1和语句2分别执行1次),obj上绑定的code被更新为401。最终,语句4的入参函数执行,输出obj.code的值为401
4)由上面的例子继续扩展
function doFoo(fn) {
this.code = 404;
fn();
}
function f() {
setTimeout(() => {
console.log(">>>" + this); // >>>[object window],语句3
this.code = 401; // 语句4
}, 0)
console.log( this.code ); // 404,语句2
}
let obj = {
code: 200,
foo: f
};
var code = 500;
doFoo( obj.foo ); // 语句1
setTimeout(()=>{console.log(obj.code)}, 0); // 200,语句5
setTimeout(()=>{console.log(window.code)}, 0); // 401,语句6
结果分析:obj.foo
为函数句柄,作为入参传入函数doFoo
,doFoo
的调用房为全局变量window,所以,语句2中doFoo对象的code是404、3、4中的this均指向window