对 js 中 this、apply、call 和闭包的理解

this

绑定到对象上的函数称为方法,在一个方法内部,this 是一个特殊变量,它始终指向当前对象。

定义一个对象:

1
2
3
4
5
6
var obj = {
text: 'hello world',
foo: function(){
console.log(this.text);
}
}

如果这样调用,显然是没问题的:

1
obj.foo();  // hello world

但是如果这样调用,在 strict 模式下会报错:

1
2
var fn = obj.foo;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined

这是因为,strict 模式下此时的 this 指向 undefined,要保证 this 指向正确,必须用 obj.xxx() 的形式调用。

如果修改了代码,又会报错:

1
2
3
4
5
6
7
8
9
10
11
'use strict';
var obj = {
text: 'hello world',
foo: function(){
return function(){
console.log(this.text);
}()
}
}
obj.foo();
// Uncaught TypeError: Cannot read property 'birth' of undefined

这是因为 this 指针只在age方法的函数内指向xiaoming,在函数内部定义的函数, this 又指向undefined了!(在非strict模式下,它重新指向全局对象window!)

修复的办法是用一个that变量首先捕获this

1
2
3
4
5
6
7
8
9
10
11
'use strict';
var obj = {
text: 'hello world',
foo: function(){
var that = this;
return function(){
console.log(that.text);
}()
}
}
obj.foo(); // hello world

apply & call

改变this指向

虽然在一个独立的函数调用中,根据是否是strict模式,this指向undefinedwindow,不过,还是可以控制this的指向的。

要指定函数的this指向哪个对象,可以用函数本身的apply方法,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。

下面用apply修复函数调用:

1
2
3
4
5
6
7
8
9
function foo(){
console.log(this.text);
}

var obj = {
text: 'hello world'
};

foo.apply(obj); // hello world

call()apply()作用类似,唯一区别是:apply()把参数打包成Array再传入,而call()把参数按顺序传入。比如调用Math.max(3, 5, 4),分别用apply()call()实现如下:

1
2
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

对普通函数调用,通常把this绑定为null

包装函数

除了改变this指向外,apply还能用来包装函数,给函数添加一些额外操作,动态改变函数的行为。

比如要统计一下代码一共调用了多少次parseInt(),可以在原有的parseInt()上添加一个计数器。

1
2
3
4
5
6
7
8
9
10
11
12
13
var count = 0;
var oldParseInt = parseInt; // 保存原函数

window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};

// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3

闭包

概念

在JS中,函数内部可以直接读取全局变量,在函数外部无法读取函数内的局部变量。函数内部声明变量的时候,一定要使用var命令。如果不用的话,实际上声明了一个全局变量。

因此,这样显然是不对的:

1
2
3
4
function f1(){
  var n=999;
}
alert(n); // error

要想得到函数内的局部变量,需要在函数的内部,再定义一个函数:

1
2
3
4
5
6
7
8
9
function f1(){
var n=999;
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1();
result(); // 999

因此,可以理解为,闭包就是能够读取其他函数内部变量的函数,是一个定义在函数内部的函数,是将函数内部和函数外部连接起来的一座桥梁。

示例

假设页面上有5个div节点,给每个节点绑定一个onclick事件,在点击div时弹出相应的数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<body>
<div>0</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<script>
var nodes = document.getElementsByTagName('div');
for(var i = 0; i < nodes.length; i++){
nodes[i].onclick = function () {
alert(i);
}
};
</script>

</body>
</html>

测试这段代码,发现无论点击哪个div,最后弹出的都是5。原因在于返回的函数引用了变量i,但它并非立刻执行。等到5个函数都返回时,它们所引用的变量i已经变成了5。

因此,返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

1
2
3
4
5
6
7
for(var i = 0; i < nodes.length; i++){
(function(n){
nodes[n].onclick = function () {
alert(n);
}
})(i)
};

这里用了一个“创建一个匿名函数并立刻执行”的语法:

1
2
3
(function (x) {
...
})(y);

更多关于立即执行函数的理解可以参考 这篇文章

应用

闭包可以用在很多地方,最大的用处有两个,一是封装私有变量(通过闭包读取函数内部的变量),二是延长局部变量的寿命(让这些变量的值始终保持在内存中)。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
var func = function(){
var x = 1;
return function(){
console.log(x);
x++;
}
}

var f = func();
f(); // 1
f(); //2

出现这种结果的原因是,执行 var f = func(); 时,f 返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量 x 一直处在这个环境里。既然局部变量所在的环境还能被外界访问,那这个变量就不会被销毁。因为闭包,局部变量的声明周期被延长。

参考

  1. JavaScript教程 . 廖雪峰
  2. 学习Javascript闭包 . 阮一峰
  3. JavaScript设计模式与开发实践 . 曾探
坚持原创技术分享,您的支持将鼓励我继续创作!