文章目录
  1. 1. this关键字的含义
  2. 2. this在不同情况下,指向各不相同
    1. 2.1. 全局环境/函数调用
    2. 2.2. 构造函数
    3. 2.3. 方法调用
    4. 2.4. Node.js
  3. 3. 使用this你需要注意
    1. 3.1. 一个常见的误解
      1. 3.1.1. 避免多层this-例一
      2. 3.1.2. 避免多层this-例二
      3. 3.1.3. 避免数组处理方法中的this
    2. 3.2. 方法的赋值表达式
    3. 3.3. 避免回调函数中的this
  4. 4. 固定this的方法
    1. 4.1. call方法
      1. 4.1.1.
    2. 4.2. apply方法
      1. 4.2.1.
      2. 4.2.2. 应用一:找出数组最大元素
      3. 4.2.3. 应用二:将数组的空元素变为undefined
      4. 4.2.4. 应用三:转换类似数组的对象
      5. 4.2.5. 应用四:绑定回调函数的对象
    3. 4.3. bind方法
      1. 4.3.1. 使用bind时需注意
        1. 4.3.1.1. 每一次返回一个新函数
        2. 4.3.1.2. bind方法的自定义代码
        3. 4.3.1.3. jQuery的proxy方法
        4. 4.3.1.4. 结合call方法使用

this关键字的含义

简单说,this就是指当前函数的运行环境。由于JavaScript支持运行环境的动态切换,所以this的指向是动态的。
所谓“运行环境”其实就是对象。可以理解成,this指函数运行时所在的那个对象。

例:有一个函数f,它同时充当a对象和b对象的方法。JavaScript允许函数f的运行环境动态切换,即一会属于a对象,一会属于b对象,这就要靠this关键字来办到。

1
2
3
4
5
6
7
8
9
10
function f(){ console.log(this.x); };
var a = {x:'a'};
a.m = f;
var b = {x:'b'};
b.m = f;
a.m() // a
b.m() // b

当f属于a对象时,this指向a;当f属于b对象时,this指向b,因此打印出了不同的值。由于this的指向可变,所以达到了运行环境动态切换的目的。

this在不同情况下,指向各不相同

全局环境/函数调用

在全局环境使用this,它指的就是顶层对象window。

1
2
3
4
5
6
console.log(this === window) // true
function f() {
console.log(this === window); // true
}
f();

不管是不是在函数内部,只要是在全局环境下运行,this就是指全局对象window。

构造函数

构造函数中的this,指的是实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var O = function(p) {
this.p = p;
};
O.prototype.m = function() {
return this.p;
};
// test
var a = new O("test case 1");
console.log ( a.p ); // test case 1
console.log ( a.m() ); // test case 1
var b = new O("test case 2");
console.log ( b.p ); // test case 2
console.log ( b.m() ); // test case 2

上面代码定义了一个构造函数O。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性;然后m方法可以返回这个p属性。

方法调用

当a对象的方法被赋予b对象,该方法就变成了普通函数,其中的this就从指向a对象变成了指向b对象。这就是this取决于运行时所在的对象的含义,所以要特别小心。如果将某个对象的方法赋值给另一个对象,会改变this的指向。

1
2
3
4
5
6
7
8
9
10
11
var o1 = new Object();
o1.m = 1;
o1.f = function (){ console.log(this.m);};
o1.f() // 1
var o2 = new Object();
o2.m = 2;
o2.f = o1.f
o2.f() // 2

f是o1的方法,但是如果在o2上面调用这个方法,f方法中的this就会指向o2。这就说明JavaScript函数的运行环境完全是动态绑定的,可以在运行时切换。

如果不想改变this的指向,可以将o2.f改写成下面这样。

1
2
3
o2.f = function (){ o1.f() };
o2.f() // 1

上面代码表示,由于f方法这时是在o1下面运行,所以this就指向o1。

有时,某个方法位于多层对象的内部,这时如果为了简化书写,把该方法赋值给一个变量,往往会得到意想不到的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*会得到一个意外的答案*/
var a = {
b : {
m : function() {
console.log(this.p);
},
p : 'Hello'
}
};
var hello = a.b.m;
hello(); // undefined

上面代码表示,m属于多层对象内部的一个方法。为求简写,将其赋值给hello变量,结果调用时,this指向了全局对象
为了避免这个问题,可以只将m所在的对象赋值给hello,这样调用时,this的指向就不会变。

1
2
var hello = a.b;
hello.m() // Hello

Node.js

在Node.js中,this的指向又分成两种情况。全局环境中,this指向全局对象global;模块环境中,this指向module.exports。

1
2
3
4
5
// 全局环境
this === global // true
// 模块环境
this === module.exports // true

在浏览器环境下,最高作用域是global作用域,这意味着变量会定义在全局变量下。
而在Node下,最高最高作用域不是global作用域,定义的变量属于Node模块。
在Node.js中怎样使用全局变量:http://stackoverflow.com/questions/10987444/how-to-use-global-variable-in-node-js

使用this你需要注意

一个常见的误解

避免多层this-例一

1
2
3
4
5
6
7
8
9
10
11
12
13
var Foo = {};
Foo.method = function() {
console.log("Foo.method中this是否指向Foo :" ,this === Foo);
console.log("Foo.method中this:" ,this);
function test() {
console.log("Foo.method中test函数的this是否指向Foo :" ,this === Foo);
console.log("Foo.method中test函数的this是否指向window :",this === window);
console.log("Foo.method中test函数的this:",this);// this 将会被设置为全局对象(浏览器环境中也就是 window 对象)
}
test();
}
Foo.method();

一个常见的误解是 test 中的 this 将会指向 Foo 对象,实际上不是这样子的。
大家也可以自己去试下,我直接贴我试的结果—
this的误解
可以看到 Foo.method 这个函数中的 this 指向的是 Foo 这个对象,
而 Foo.method 中 test 函数的 this 指向的并不是Foo这个对象,而是 window 这个对象。

解决方法:为了在 test 中获取对 Foo 对象的引用,我们需要在 method 函数内部创建一个局部变量指向 Foo 对象。

1
2
3
4
5
6
7
8
9
10
11
var Foo = {};
Foo.method = function() {
console.log("Foo.method中this是否指向Foo :" ,this === Foo);
var that = this;
function test() {
console.log("Foo.method中test函数的that是否指向Foo :" ,that === Foo);
}
test();
}
Foo.method();

that 只是我们随意起的名字,不过这个名字被广泛的用来指向外部的 this 对象。
this的误解

避免多层this-例二

与例一类似的:

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
f1: function() {
console.log(this);
var f2 = function() {
console.log(this);
}();
}
}
o.f1()
// Object
// Window

this的误解
两层this,结果运行后,第一层指向该对象,第二层指向全局对象。
解决方法(同例一):在第二层改用一个指向外层this的变量, 定义变量that,固定指向外层的this,然后在内层使用that,就不会发生this指向的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object

this的误解

避免数组处理方法中的this

数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this。

1
2
3
4
5
6
7
8
9
10
11
12
13
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v+' '+item);
});
}
}
o.f()
// undefined a1
// undefined a2

this的误解
上面代码中,foreach方法的参数函数中的this,其实是指向window对象,因此取不到o.v的值。

解决方案一:使用中间变量 - 这与避免多层this解决方法一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
var that = this;
this.p.forEach(function (item) {
console.log(that.v+' '+item);
});
}
}
o.f()
// hello a1
// hello a2

this的误解

解决方案二:将this当作foreach方法的第二个参数,固定它的运行环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v+' '+item);
}, this);
}
}
o.f()
// hello a1
// hello a2

this的误解

方法的赋值表达式

另一个看起来奇怪的地方是函数别名,也就是将一个方法赋值给一个变量。

1
2
3
4
5
6
7
8
9
var Foo = {};
Foo.method = function() {
console.log("this是否指向Foo :" ,this === Foo);
console.log("this指向:" ,this);
}
Foo.method();
var test = Foo.method;
test();

this的误解

上例中,test 就像一个普通的函数被调用;因此,函数内的 this 将不再被指向到 Foo 对象。

虽然 this 的晚绑定特性似乎并不友好,但是这确实基于原型继承赖以生存的土壤。

1
2
3
4
5
6
7
8
9
function Foo() {}
Foo.prototype.method = function() {
console.log("this指向:" ,this);
};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();

this的误解

当 method 被调用时,this 将会指向 Bar 的实例对象。

避免回调函数中的this

回调函数中的this往往会改变指向,最好避免使用。

1
2
3
4
5
6
7
var o = new Object();
o.f = function (){
console.log(this === o);
}
o.f() // true

this的误解
上面代码表示,如果调用o对象的f方法,其中的this就是指向o对象。

但是,如果将f方法指定给某个按钮的click事件,this的指向就变了。
$(“#button”).on(“click”, o.f);

点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的DOM对象,因为f方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。

为了解决这个问题,可以采用下面的一些方法对this进行绑定,也就是使得this固定指向某个对象,减少不确定性。

固定this的方法

this的动态切换,固然为JavaScript创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this固定下来,反正出现意想不到的情况。

JavaScript提供了call、apply、bind这三个方法,来固定this的指向。

call方法

函数的call方法,可以改变指定该函数内部this的指向,然后再调用该函数。它的使用格式如下。

func.call(thisValue, arg1, arg2, …)

thisValue:第一个参数是this所要指向的那个对象。(如果this所要指向的那个对象,设定为null或undefined,则等同于指定全局对象。)
arg1,arg2, …:函数调用时所需的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
var n = 123;
var o = { n : 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(o) // 456

上面代码中,a函数中的this关键字,如果指向全局对象,返回结果为123。
如果使用call方法,将this关键字指向o对象,返回结果为456。

apply方法

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。它的使用格式如下。

func.apply(thisValue, [arg1, arg2, …])

thisValue:第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。
[arg1, arg2, …]:第二个参数是一个数组,该数组的所有成员依次作为参数,传入原函数。
原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。

1
2
3
4
function f(x,y){ console.log(x+y); }
f.call(null,1,1) // 2
f.apply(null,[1,1]) // 2

上面的f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。

利用这一点,可以做一些有趣的应用。

应用一:找出数组最大元素

JavaScript不提供找出数组最大元素的函数。结合使用apply方法和Math.max方法,就可以返回数组的最大元素。

1
2
3
4
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a)
// 15

上例等同于这样写

1
2
Math.max(10, 2, 4, 15, 9);
// 15

关于Math.max:

定义和用法:
max() 方法可返回两个指定的数中带有较大的值的那个数。

语法:
Math.max(x…)

参数:
0 或多个值。在 ECMASCript v3 之前,该方法只有两个参数。

返回值:
参数中最大的值。如果没有参数,则返回 -Infinity。如果有某个参数为 NaN,或是不能转换成数字的非数字值,则返回 NaN。

应用二:将数组的空元素变为undefined

通过apply方法,利用Array构造函数将数组的空元素变成undefined。

1
2
Array.apply(null, ["a",,"b"])
// [ 'a', undefined, 'b' ]

关于Array构造函数:

创建 Array 对象的语法:
new Array();
new Array(size);
new Array(element0, element1, …, elementn);

参数:
参数 size 是期望的数组元素个数。返回的数组,length 字段将被设为 size 的值。
参数 element …, elementn 是参数列表。当使用这些参数来调用构造函数 Array() 时,新创建的数组的元素就会被初始化为这些值。它的 length 字段也会被设置为参数的个数。

返回值:
返回新创建并被初始化了的数组。
如果调用构造函数 Array() 时没有使用参数,那么返回的数组为空,length 字段为 0。
当调用构造函数时只传递给它一个数字参数,该构造函数将返回具有指定个数、元素为 undefined 的数组。
当其他参数调用 Array() 时,该构造函数将用参数指定的值初始化数组。
当把构造函数作为函数调用,不使用 new 运算符时,它的行为与使用 new 运算符调用它时的行为完全一样。

空元素与undefined的差别在于,数组的foreach方法会跳过空元素,但是不会跳过undefined。
因此,遍历内部元素的时候,会得到不同的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = ["a",,"b"];
function print(i) {
console.log(i);
}
a.forEach(print)
// a
// b
Array.apply(null,a).forEach(print)
// a
// undefined
// b

应用三:转换类似数组的对象

利用数组对象的slice方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组。

1
2
3
4
5
6
7
8
9
10
11
Array.prototype.slice.apply({0:1,length:1})
// [1]
Array.prototype.slice.apply({0:1})
// []
Array.prototype.slice.apply({0:1,length:2})
// [1, undefined]
Array.prototype.slice.apply({length:1})
// [undefined]

关于slice方法:

定义和用法:
slice() 方法可从已有的数组中返回选定的元素。

语法
arrayObject.slice(start,end)

参数
start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

返回值
返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。

说明
请注意,该方法并不会修改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该使用方法 Array.splice()。

slice例一

1
2
3
4
5
6
7
8
9
10
11
var arr = new Array(3)
arr[0] = "George"
arr[1] = "John"
arr[2] = "Thomas"
console.log(arr)
console.log(arr.slice(1))
console.log(arr)
// ["George", "John", "Thomas"]
// ["John", "Thomas"]
// ["George", "John", "Thomas"]

slice例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr = new Array(6)
arr[0] = "George"
arr[1] = "John"
arr[2] = "Thomas"
arr[3] = "James"
arr[4] = "Adrew"
arr[5] = "Martin"
console.log(arr)
console.log(arr.slice(2,4))
console.log(arr)
// ["George", "John", "Thomas", "James", "Adrew", "Martin"]
// ["Thomas", "James"]
// ["George", "John", "Thomas", "James", "Adrew", "Martin"]

应用四:绑定回调函数的对象

之前的按钮点击事件的例子,可以改写成

1
2
3
4
5
6
7
8
9
10
11
12
var o = new Object();
o.f = function (){
console.log(this === o);
}
var f = function (){
o.f.apply(o);
// 或者 o.f.call(o);
};
$("#button").on("click", f);

点击按钮以后,控制台将会显示true。由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。
更简洁的写法是采用下面介绍的bind方法。

bind方法

bind方法就是单纯地将函数体内的this绑定到某个对象,然后返回一个新函数。它的使用格式如下。

func.bind(thisValue, arg1, arg2,…)

它比call方法和apply方法更进一步的是,除了绑定this以外,还可以绑定原函数的参数。

请看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var o1 = new Object();
o1.p = 123;
o1.m = function (){
console.log(this.p);
};
o1.m() // 123
var o2 = new Object();
o2.p = 456;
o2.m = o1.m;
o2.m() // 456
o2.m = o1.m.bind(o1);
o2.m() // 123

上面代码使用bind方法将o1.m方法绑定到o1以后,在o2对象上调用o1.m的时候,o1.m函数体内部的this.p就不再到o2对象去寻找p属性的值了。

如果bind方法的第一个参数是null或undefined,等于将this绑定到全局对象,函数运行时this指向全局对象(在浏览器中为window)。

1
2
3
4
5
function add(x,y) { return x+y; }
var plus5 = add.bind(null, 5);
plus5(10) // 15

使用bind时需注意

每一次返回一个新函数

bind方法每运行一次,就返回一个新函数,这会产生一些问题。
比如,监听事件的时候,不能写成下面这样。

1
element.addEventListener('click', o.m.bind(o));

上面代码表示,click事件绑定bind方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的。

1
element.removeEventListener('click', o.m.bind(o));

正确的方法是写成下面这样:

1
2
3
4
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
bind方法的自定义代码

对于那些不支持bind方法的老式浏览器,可以自行定义bind方法。

1
2
3
4
5
6
7
8
9
10
if(!('bind' in Function.prototype)){
Function.prototype.bind = function(){
var fn = this;
var context = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
return function(){
return fn.apply(context, args);
}
}
}
jQuery的proxy方法

除了用bind方法绑定函数运行时所在的对象,还可以使用jQuery的$.proxy方法,它与bind方法的作用基本相同。

1
$("#button").on("click", $.proxy(o.f, o));

上面代码表示,$.proxy方法将o.f方法绑定到o对象。

结合call方法使用

利用bind方法,可以改写一些JavaScript原生方法的使用形式,以数组的slice方法为例。

1
2
3
4
5
6
7
[1,2,3].slice(0,1)
// [1]
// 等同于
Array.prototype.slice.call([1,2,3], 0, 1)
// [1]

上面的代码中,数组的slice方法从[1, 2, 3]里面,按照指定位置和长度切分出另一个数组。这样做的本质是在[1, 2, 3]上面调用Array.prototype.slice方法,因此可以用call方法表达这个过程,得到同样的结果。

call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。

1
2
3
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

可以看到,利用bind方法,将[1, 2, 3].slice(0, 1)变成了slice([1, 2, 3], 0, 1)的形式。
这种形式的改变还可以用于其他数组方法。

1
2
3
4
5
6
7
8
9
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]
pop(a)
a // [1, 2, 3]

如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以被改写。

1
2
3
4
5
6
7
8
9
function f(){
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f,o)() // 123

上面代码表示,将Function.prototype.call方法绑定Function.prototype.bind以后,bind方法的使用形式从f.bind(o),变成了bind(f, o)。


参考文献:
[1] 面向对象编程概述 - JavaScript 标准参考教程(alpha)
[2] this 的工作原理 - JavaScript 秘密花园
[3] Global Objects - Node.js v0.10.31 Manual & Documentation
[4] How to use global variable in node.js? - stackoverflow

文章目录
  1. 1. this关键字的含义
  2. 2. this在不同情况下,指向各不相同
    1. 2.1. 全局环境/函数调用
    2. 2.2. 构造函数
    3. 2.3. 方法调用
    4. 2.4. Node.js
  3. 3. 使用this你需要注意
    1. 3.1. 一个常见的误解
      1. 3.1.1. 避免多层this-例一
      2. 3.1.2. 避免多层this-例二
      3. 3.1.3. 避免数组处理方法中的this
    2. 3.2. 方法的赋值表达式
    3. 3.3. 避免回调函数中的this
  4. 4. 固定this的方法
    1. 4.1. call方法
      1. 4.1.1.
    2. 4.2. apply方法
      1. 4.2.1.
      2. 4.2.2. 应用一:找出数组最大元素
      3. 4.2.3. 应用二:将数组的空元素变为undefined
      4. 4.2.4. 应用三:转换类似数组的对象
      5. 4.2.5. 应用四:绑定回调函数的对象
    3. 4.3. bind方法
      1. 4.3.1. 使用bind时需注意
        1. 4.3.1.1. 每一次返回一个新函数
        2. 4.3.1.2. bind方法的自定义代码
        3. 4.3.1.3. jQuery的proxy方法
        4. 4.3.1.4. 结合call方法使用