闭包详解
1. 闭包简介
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
一个函数内部包含嵌套函数并且嵌套函数被返回出来,那么内部函数被保存到了外部,就会生成闭包,此时边有一个外部的引用指向这个嵌套函数。闭包是由函数以及声明该函数的词法环境组合而成的。
【例】如下,b函数被保存到了外部,输出结果为101、102
function a () {
var num = 100;
function b () {
num++;
console.log(num);
}
return b;
}
var glob = a();
glob();//101
glob();//102
2. 闭包缺点
闭包会导致原有作用域链不能释放,造成内存泄漏。
3. 闭包作用
闭包将函数与其所操作的某些数据(环境)关联起来。类似于面向对象编程。
可以利用闭包来实现类似Java中的私有修饰符的功能。
3.1. 实现公有变量
【例】函数累加器
function add () {
var count = 0;
function sum () {
count++;
console.log(count + "\n");
}
return sum;
}
var counter = add();
counter();
counter();
3.2. 可以做缓存
【例1】简易存储结构,此时的 obj
中的方法被返回出来,可以临时存储变量 food
function eater () {
var food = ""; // honey缓存在food中
var obj = {
eat: function () {
console.log("I am eating " + food);
food = "";
},
push: function (myfood) {
food = myfood;
}
}
return obj;
}
var eater1 = eater();
eater1.push("honey");
eater1.eat();
3.3. 可以实现封装、属性私有化
【例】此时的 privateCounter
就是一个私有属性,可以通过函数接口调用,但是无法直接访问。
var Counter = (function () {
var privateCounter = 0; // 私有属性,无法直接访问
function changeBy (val) {
return privateCounter += val;
}
return {
incre: function (val) {
console.log(changeBy(val));
},
decre: function (val) {
console.log(changeBy(-val));
},
}
})();
Counter.incre(3);
Counter.decre(5);
3.4. 模块化开发,防止污染全局变量
闭包既能重复使用局部变量,又不污染全局!
4. 闭包的this指向
【例2】题外话,代码注释中提了个问题,为什么那里的this指向window?
因为函数最终是在window(node中是global)上完成调用的。
简单的作用域问题是笔试面试常考的题目,在JavaScript中这很关键。
function memory (f) {
var cache = {};
return function () {
// 传入类数组 3, 4, 5
var key = arguments.length + ":" + Array.prototype.join.call(arguments, ',');
console.log(typeof (key) + ":" + key);
if (key in cache) { // 如果值在缓存,直接读取返回
console.log("值在缓存,直接读取返回:" + cache[key]);
return cache[key];
} else { // 否则执行计算,并把结果放到缓存,此处的 f 指阶乘运算
// 此处的this指向window,这是为什么?
console.log(this);
cache[key] = f.apply(this, arguments);
// 此处的arguments是一个类数组,但是阶乘运算只会取第一位进行阶乘运算
console.log(cache[key]);
return cache[key];
}
}
}
var factorial = function (n) { // 阶乘
return (n <= 1) ? 1 : n * factorial(n - 1);
}
var factorialWithMemory = memory(factorial);
factorialWithMemory(3, 4, 5); //3:3,4,5
【例3】闭包指向附加示例
let num = 1,
obj = {
num: 2,
getNum: function () {
return (function () {
return this.num;
})();
}
}
console.log(obj.getNum());
// 在浏览器输出1,在ndoe输出undefined
【例3】里面的自执行匿名函数不属于任何对象,他不是一个对象的方法(无法使用点运算符调用)。在非严格模式中,无指向的、函数内部的this,指向window。this的值取决于调用上下文,如果一个函数不是作为某个对象的方法被调用,那么this就是global或则window。否则就是该对象。对象实例化之后的this指的是本身,未实例化则this指的是调用者的对象。
匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。new Function
中的this指全局对象,eval中的this指调用上下文中的this,要是想要改变这情况,可以这么写:
var num = 1,
obj = {
num: 2,
getNum: function () {
return (function (self) {
return self.num;
})(this);
}
}
console.log(obj.getNum()); // 2
function m (method) {
var a = {};
a.m = method;
method();//window
a.m();//对象a
}
m(function () { console.log(this) });
let m = () => {
console.log(this)
};
let a = { name: "Hui" };
m(); //打印结果为window(node中是一个空对象)
m.call(a); //打印结果为window(node中是一个空对象)
// 箭头函数中的this是固定的,不可变
// 所以call、apply、bind 的第一个参数会被忽略
var m = () => {
console.log(this)
// 定义了一个箭头函数为m,里面的this将会一直是创建时的this,是啥呢?
// 此时全局中的this是window(node中是{})
};
var obj = {
a: m,
b: function () {
m();
},
c: function () {
var m2 = () => {
console.log(this)
}; //每次调用都会新定义这个箭头函数
m2();
}
}
obj.a(); // Window (node中是一个空对象)
obj.b(); // Window (node中是一个空对象)
// 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
// 是b方法,b方法的this指向全局对象
obj.c(); // {a: ƒ, b: ƒ, c: ƒ}
var obj2 = {};
obj2.c = obj.c;
obj2.c(); // {c: ƒ}
参考
总结就是函数中的this指向调用它的对象,但在匿名执行函数中明显没有人调用它,或者说默认是全局对象调用了他。
之所以指向 Window
和《ECMAScript® 2015 Language Specification》有关,它指出了,如果thisArguments
是undefined
,则thisValue
就是 [[globalThis]]
。
引入this的初衷就是想在原型继承的情况下,拿到函数的调用者,如果函数没有指明调用者呢,那就让this指向全局对象。更多的情况在注视中写了。
4.1. 特殊情况
在node,以及在严格模式中,各种this的指向可能和浏览器中不一样。
比如在node中输出this是{}
,而不是 Object [global]
,当然在匿名执行函数中shuchuthis指向的是 Object [global]
。
function as (params) {
'use strict';
console.log(this); // undefined
console.log(this === global); // false
}
as()
function asas (params) {
console.log(this); // Object [global]
console.log(this === global); // true
}
asas()
// 这是一个表达式,输出的结果和在Chrome中不一样
var m = () => { console.log(this); }
m(); // {}
console.log(globalThis); // Object [global]
// 这是一个匿名函数
(function () {
console.log(this); // Object [global]
})()
// 这是一个函数声明,这代表了global对象的方法,所以指向global
function as () {
console.log(this);
}
as(); // Object [global]
5. 使用闭包注意点
- this指向问题
- 内存消耗问题(建议在退出函数前将不使用的局部变量删除)
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
- 外部变量的值是否改变
function test () {
var array = []
for (var count = 0; count < 5; count++) {
array[count] = function (value) { // 立即执行函数
return function () {
console.log(value)
}
}(count)//此处若不是用立即执行函数则输出的结果都是5
}
return array
}
var result = test()
result[0](); // 0
result[1](); // 1
result[2](); // 2
result[3](); // 3
result[4](); // 4
参考:
6. 立即执行函数
针对初始化功能的函数(只执行一次,执行后再也不需要,不希望它继续占内存)
为什么这样做就相当于立即执行函数?
因为正常执行函数都是函数的引用或者说名称加上执行符号()
,例如test();
这里所说的函数的引用或者说名称实际上代表了函数的表达式,在函数表达式后面加上执行符号就代表执行函数,此处
(function () {}());// W3C建议
(function () {})();
【例】1
//可以执行,因为var 变量名 = function(){}相当于函数表达式
//执行之后再访问test1为undefined,
//因为function作为立即执行函数执行后放弃了表达式的名称test1
var test1 = function () {
console.log('doing'); // doing
}();
console.log(test1); // undefined
【例】2
//会报错,不能执行,因为此处 function 是函数声明,而不是表达式
function test2 () {
document.write("doing");
} ();
// 这么写本身就存在语法错误了!
【例】3
//不执行,也不报错,对比test2由于立即执行符号()传了参数
//所以系统会把函数后面的立即执行符号看作单独的一行表达式
// function test3 (a, b, c, d) {
// document.write(a + b + c + d);}
// (1, 2, 3, 4);
// 此处与直接写 1, 2, 3, 4 其实一样
function test3 (a, b, c, d) {
document.write(a + b + c + d + "doing");
} (1, 2, 3, 4);
【例】4
//能执行,因为+把函数声明转换为表达式,同时! - || &&都可以
+ function test4 () {
console.log('doing');
}();