MDN 定义如下:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
闭包的特性
如下面的例子:
function person() {
var name = "John";
function showName() {
console.log(name); // 'John'
}
return showName;
}
const getPerson = person();
getPerson();
showName()
函数中并没有定义 name
变量,但是却可以访问到外部函数中 name
变量的值,这就是利用了闭包的特性,即嵌套函数可访问声明于它们外部作用域的变量。
词法作用域
词法作用域是静态作用域,通俗讲就是它可以访问到变量的范围。
var name = 'John';
function greeting() {
let message = 'Hi';
console.log(message + ' '+ name);
}
在上面的例子中:
name
是个全局变量,它可以在任意地方访问到,包括函数 greeting()
message
是个局部变量,只能在函数 greeting()
中访问到闭包在循环中
下面这段代码很常见:
for (var index = 1; index <= 3; index++) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
}
这里输出如下:
// after 4 second(s):4
// after 4 second(s):4
// after 4 second(s):4
这是因为,var
定义的变量是个全局变量,当 setTimeout
调用时,全局变量index
的值被修改为 4,所以每次打印中获取到的 index
值是 4。
针对上面的问题,可以使用闭包和立即执行函数(IIFE)搭配解决。
闭包
for (var index = 1; index <= 3; index++) {
(function (index) {
setTimeout(function () {
console.log('after ' + index + ' second(s):' + index);
}, index * 1000);
})(index);
}
// 输出
// after 1 second(s):1
// after 2 second(s):2
// after 3 second(s):3
上面我们在 setTimeout
函数外使用了立即执行函数,立即执行函数 index
是局部变量,与全局变量 index
处于不同的作用域,此时 setTimeout
函数中能拿到立即执行函数中的 index
变量的值,因此能够得到正确的输出结果。
闭包的应用
缓存变量的值
闭包和函数内传入的值有关联。下面的代码中 createInc
函数调用后,返回一个箭头函数,箭头函数中的 index
和 startValue
是外部变量,箭头函数内可以拿到两个变量的值,并且可以对变量的值进行修改。
我们可以使用闭包的特性缓存变量的值:
function createInc(startValue) {
let index = -1;
return (step) => {
startValue += step;
index++;
return [index, startValue];
};
}
const inc = createInc(5);
console.log(inc(2)); // [0, 7]
console.log(inc(2)); // [1, 9]
console.log(inc(2)); // [2, 11]
用闭包模拟私有方法
我们可以使用闭包来模拟私有方法。私有方法可以限制对代码的访问,而且可以用于管理全局变量命名,避免扰乱公共代码。
let Counter = (function() {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1
观察上面代码,Counter
函数立即调用之后,返回一个对象,其中包含三个函数: increment
、decrement
、value
,它们都能访问 Counter
函数内部的私有项:privateCounter
的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
小练习
下面代码输出什么?
const foo = (function() {
var v = 0
return () => {
return v++
}
}())
for (let i = 0; i < 10; i++) {
foo()
}
console.log(foo())
答案:10。因为 foo
是个立即执行函数,返回的是一个箭头函数。当的 foo
调用时,就是箭头函数调用,箭头函数执行了 10 次,其中变量 v 自增到 9,之所以输出 10 是因为最后一次 console.log
中的调用。
函数的调用方式决定了 this
的值(运行时绑定)。this
不能在执行期间被赋值,并且在每次函数被调用时 this
的值也可能会不同。
this
的指向有以下七种:
全局上下文
this
指向全局对象
console.log(this === window); // true
a = 37;
console.log(window.a); // 37
函数上下文
this
指向取决于函数被调用的方式:
this
指向该对象严格模式下,指向全局对象,浏览器中就是 window
非严格模式下,为 undefined
call
、apply
、bind
调用,this
指向绑定的对象new
,this
指向新的对象function f1(){
return this;
}
//在浏览器中:this 指向全局对象 window
f1() === window;
//在 Node 中:this 指向 global
f1() === global;
// 严格模式下:this 指向 undefined
function f2(){
"use strict";
return this;
}
f2() === undefined; // true
// call 方法:this 指向传入的指定对象 o
function add(c, d) {
return this.a + this.b + c + d;
}
var o = {a: 1, b: 3};
add.call(o, 5, 7); // 16
类上下文
this
指向类。在类的构造函数中,this
是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中:
class Car {
constructor() {
// 使用 bind() 方法改变 this 指向
this.sayBye = this.sayBye.bind(this);
}
sayHi() {
console.log(`Hello from ${this.name}`);
}
sayBye() {
console.log(`Bye from ${this.name}`);
}
get name() {
return 'Ferrari';
}
}
class Bird {
get name() {
return 'Tweety';
}
}
const car = new Car();
const bird = new Bird();
// this 指向调用者
car.sayHi(); // Hello from Ferrari
bird.sayHi = car.sayHi;
bird.sayHi(); // Hello from Tweety
// bind() 方法改变了 this 指向,this 指向类 Car
bird.sayBye = car.sayBye;
bird.sayBye(); // Bye from Ferrari
箭头函数
this
与封闭词法环境的 this
保持一致。在全局代码中,它将被设置为全局对象。
var window = this;
var foo = (() => this);
console.log(foo() === window); // true
原型链中的 this
如果该方法存在于一个对象的原型链上,那么 this
指向的是调用这个方法的对象。
var o = {
f: function() {
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
作为一个 DOM 事件处理函数
当函数被用作事件处理函数时,它的 this
指向触发事件的元素。
function bluify(e) {
console.log(this === e.currentTarget); // true
this.style.backgroundColor = 'blue'
}
// 获取 id 为 test 的 button
let testBtn = document.getElementsById('test');
// 将 bluify 作为元素的点击监听函数,当元素被点击时,就会变成蓝色
testBtn.addEventListener('click', bluify, false);
getter 与 setter 中的 this
用作 getter
或 setter
的函数都会把 this 绑定到设置或获取属性的对象。
var o = {
a: 1,
b: 2,
get sum() {
return (this.a + this.b);
}
};
console.log(o.sum); // 输出 3
小练习
给出下面代码的输出结果:
var length = 10;
function fn() {
return this.length + 1;
}
var obj = {
length: 5,
test1: function() {
return fn();
}
};
obj.test2 = fn;
console.log(obj.test1.call()); // (1)
console.log(obj.test1()); // (2)
console.log(obj.test2.call()); // (3)
console.log(obj.test2()); // (4)
答案:输出如下:
this
指向 window
, 因为 call
方法没有执行具体要绑定的对象。this
指向 window
,返回的 fn
函数的 this
指向 window
this
指向 obj
,此处函数作为对象 obj
的方法调用类数组不是数组,是对象。
例如 document.getElementsByTagName()
返回的 NodeList
或 arguments
等 JavaScript 对象,有与数组相似的行为,但它们并不共享数组的所有方法。arguments 对象提供了 length 属性,但没有实现如 forEach() 等数组方法。不能直接在类数组对象上调用数组方法。
在函数中,arguments
对象代表函数实参。arguments
对象是一个类数组对象,类数组不是数组,因此它不是 Array
类型的实例,它是 Object
类型的实例。
function add (one, two) {
console.log(arguments);
console.log(Array.isArray(arguments));
console.log(Object.prototype.toString.call(arguments));
}
add(1, 2);
结果如下:
arguments
特点arguments
既然是类数组对象,它有两个特点:
[]
获取参数, 如 arguments[0]
length
属性获取实参的个数。如 arguments.length
function add() {
let sum = 0;
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}
console.log(add(1, 2)); // 3
类数组转成数组
我们可以使用三种方式将类数组转成数组:
Array.from()
slice()
,使用以下两种方式都可以:Array.prototype.slice.call(arguments)
[].slice.call(arguments);
// 扩展运算符
function checkArgs() {
return [...arguments];
};
let result = checkArgs(1, 2);
console.log(result); // [1, 2]
// Array.from()
function checkArgs() {
return Array.from(arguments);
};
let result1 = checkArgs(1, 2);
console.log(result1); // [1, 2]
// slice()
function checkArgs() {
return Array.prototype.slice.call(arguments);
};
let result2 = checkArgs(1, 2);
console.log(result2); // [1, 2]
阅读量:176
点赞量:0
收藏量:0