javascript:this 关键字

前言

看过阮一峰的关于 this 的教程,讲了很多比较好的例子,但没有对其本质的东西解释清楚,而且部分例证存在问题;于是,打算重写本章节,从this的本质入手;

本文为作者的原创作品,转载需注明出处;

References

Javascript中this关键字详解
jQuery Fundamentals Chapter - The this keyword

this 是什么?

this可以理解为一个指针,指向调用对象;

判断 this 是什么的四个法则

官网定义

先来看第一段官方的解释,

In JavaScript, as in most object-oriented programming languages, this is a special keyword that is used within methods to refer to the object on which a method is being invoked. The value of this is determined using a simple series of steps:

  1. If the function is invoked using Function.call or Function.apply, this will be set to the first argument passed to call/apply. If the first argument passed to call/apply is null or undefined, this will refer to the global object (which is the window object in Web browsers).
  2. If the function being invoked was created using Function.bind, this will be the first argument that was passed to bind at the time the function was created.
  3. If the function is being invoked as a method of an object, this will refer to that object.
  4. Otherwise, the function is being invoked as a standalone function not attached to any object, and this will refer to the global object.

大致翻译如下,
this是这么一个特殊的关键字,它是用来指向一个当前正在被调用( a being invoked )方法的调用对象的;( 等等,这句话其实隐藏了一个非常关键的信息,那就是this是在运行期 生效的,怎么生效的?在运行期this被赋值,将某个对象赋值给this,与声明期无关,也就是说,this是运行期相关的 );this的赋值场景,归纳起来,分为如下四种情况,

  1. 如果方法是被Function.call或者Function.apply调用执行…. bla..bla..
    参考 function prototype call 小节

  2. 如果是被Function.bind… bla…bla
    参考 function prototype bind 小节

  3. 如果某个方法在运行期是被一个对象( an object )调用( 备注:这里为了便于理解,我针对这种情况,自己给起了个名称叫关联调用 ),在运行期,会将该 object 的引用赋值给该方法的this
    备注:被一个对象调用?何解?其实就是指语句obj.func(),这个时候,func()方法内部的this将会被赋值为obj对象的引用,也就是指向obj

  4. 如果该方法在运行期被当做一个没有依附在任何 object 上的一个独立方法被调用(is being invoked as a standalone function not attached to any object ),那么该方法内部的this将会被赋值为全局对象(在浏览器端就是 windows )
    独立方法 ( standalone function )?在运行期,如果func方法被obj关联调用的,既是通过obj.func()的方式,那么它就不是standalone的;如果是直接被调用,没有任何对象关联,既是通过func()调用,那么这就是standalone的。

this 运行期相关

官网定义 2

再来看另外一句非常精炼的描述,来加深理解

The this keyword is relative to the execution context, not the declaration context.

this关键字与运行环境有关而与声明环境无关;(补充,而作用域链闭包是在函数的声明期创建的,参考创建时机)

补充,是如何与函数的运行期相关的,参考this 指针运行时赋值

我的补充

法则 #3 和 #4,大多数情况都非常容易理解,有几种情况需要特别注意,

  1. 函数嵌套
    需要注意的是object对象中的函数内部再次嵌套函数的情况,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var name = "windows";  

    var obj = {

    name:"object",

    f1:function(){

    console.log("this: "+this.name)

    function f2(){

    console.log("this: " + this.name)
    }

    f2();
    }
    };

    执行

    1
    2
    3
    > obj.f1();
    this: object
    this: windows

    可以看到,在运行期,被调用函数 f1() 中的this指向 obj,而被调用函数 f2() 中的this指向的是 windows ( global object );因为 _f1_ 函数在当前的运行时中是通过 obj.f1() 进行的关联调用,所以,根据定义 #3,在当前的运行期间f1() 内部的 this 是指向 obj 对象的( 通过将 obj 的引用直接赋值给 this ),而, _f2_ 函数在运行期是没有与其它 object 进行关联调用,所以,在当前的运行时期,_f2_ 是一个 standalone 的函数,所以,根据定义 #4,在当前的运行期间f2() 的内部this是指向 windows 的。(注意,这里我反复强调当前运行期间,是因为this是在运行时被赋值的,所以,要特别注意的是,即使某个函数的定义不变,但在不同的执行环境(运行环境)中,this是会发生变化;)

  2. 回调函数
    参看函数回调场景-1函数回调场景-2

  3. 函数赋值
    参看将函数赋值-standalone以及相关变种章节

可见,要判断this运行期到底指的是什么,并没有那么容易,但是,只要牢牢的把握好两点,就可以迎刃而解,

  • this运行期相关的
    更确切的说,this是在运行期被赋值的,所以,它的值是在运行期动态确定的。
  • this是否与其它对象关联调用
    这里的关联调用指的是 javascript 的一种语法,既是调用语句显式的写为obj.func(),另外需要注意的是,javascript 方法的调用不会隐式的隐含 this。只要没有显式的关联调用,那么就是standalone的调用,就符合法则 #4,所以,this指向 Global Object

this 的 Object

注意,this定义中所指的Object指的是 javascriptObject 类型,既是通过

1
2
3
var o1 = {};
var o2 = new Object();
var o3 = Object.create(Object.prototype);

这样的方式构建出来的对象;

备注,最开始,自己有个思维的误区,认为既然 javascript 一切皆为对象,那么this指针是指向对象的,那么是不是也可以指向FunctionNumber等对象?答案是否定的。

起初,我是按照上面的逻辑来理解的,直到当我总结到bind 是如何实现的小节后,发现Function对象在调用方法属性bind的时候,bind方法内部的this指向的是Function,这才恍然大悟,thisObject实际上是可以指向任何 javascript Object的,包括 ObjectFunction 等。

this 是变化的

我们来看这样一个例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var C = "王麻子";

var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};

var B = {
name: '李四'
};

// 执行,
> A.describe();
'张三'

> B.describe = A.describe;
> B.describe()
'李四'

> var describe = A.describe;
> describe();
'王麻子'

可以看到,虽然 A.describe 方法的定义不变,但是其运行时环境发生了变化,this 的指向也就发生了变化。

1
2
3
> B.describe = A.describe;
> B.describe()
'李四'

在运行时,相当于运行的是 B 的 describe 方法

1
2
3
> var describe = A.describe;
> describe();
'王麻子'

在运行时,相当于运行的是 windows 的 describe 方法

方法调用没有隐含 this

经常写 Java 代码的原因,经常会习惯性的认为只要在对象方法里面调用某个方法或者属性,隐含了 this,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person{

String name;

public String getName(){
return name;
}

public String getName2(){
return this.name;
}

}

而 Javascript 实际上并没有这种隐含的表达方式;详细验证过程参考将函数赋值-standalone

关联调用 - 容易混淆的场景

this 是什么章节中,为了方便对 _#3_ 进行描述,我起了个名字叫做 关联调用 ;那么有些情况看似是 关联调用,实则不然;

我们有一个标准的对象,定义如下,

1
2
3
4
5
6
7
var name = "windows";
var obj = {
name: "obj",
foo: function () {
console.log("this: "+ this.name);
}
};

通过标准的 关联调用 的方式,我们进行如下的调用,

1
2
> obj.foo() 
'this: obj'

根据法则 #3 既 关联调用 的定义,得到 this -> obj;如果事事都如此的简单,如此的标准,那可就好了,总会有些让人费解的情况,现在来看看如下的一些特殊的例子,加深对 关联调用 的理解。

将函数赋值 - standalone

1
2
3
> var fooo = obj.foo
> fooo();
'this: windows'

输出的 windows,既是 this -> global object,而不是我们期望的 obj;为什么?原因是,obj.foo 其实是 foo 函数的函数地址,通过 var fooo = obj.foo 将该函数的地址赋给了变量 fooo,那么当执行

1
> fooo();

的时候,fooo() 执行的是是一个standalone的方法,根据法则 #4,所以该方法内部的this指向的是 Global Object;注意,obj.foo 表示函数 foo 的入口地址,所以,变量 fooo 等价与 foo 函数。

备注:由于受到写 Java 代码习惯的原因,很容易将这里解释为默认执行的是this.fooo()fooo() 的调用隐含了this,因此就会想到,由于this指向的 Global Object,所以这里当然返回的就是this: windows;但是,这样解释,是不对的,因为 Javascript 压根没有这种隐含this的概念,参看用例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var name = "windows";

var o = {

name : "o",

f2 : function(){
console.log( "o -> f2");
console.log( "this: "this.name );
},

f : function(){

console.log("f.this -> " + this.name);

var f2 = function(){
console.log( "f -> f2");
console.log( this.name );
}

f2(); // f -> f2

this.f2(); // o -> f2

}

}

可以看到,在 o.f() 函数中,如果 f2() 的调用隐含了this,那么 f2()this.f2() 两者调用应该是等价的;但是,在实际执行过程中,f2()this.f2() 执行的是两个截然不同的方法,因此 f2()this.f2(),所以 f2() 并没有隐示的表示为 this.f2()

将函数赋值变种 - 匿名 standalone 函数立即执行

1
2
> (obj.foo = obj.foo)() 
'this: windows'

首先,立即执行 foo 函数,然后将 foo 函数赋值给对象 obj 对象的 foo 属性;等价于执行如下的代码,

1
2
3
4
5
var name = "windows";    
var obj = { name : "obj" };
(obj.foo = function () {
console.log("this: " + this.name);
})();

输出,

1
'this: windows'

可以看到,this -> global object,这里为什么指向的是 global object?其实这里的立即执行过程,就是执行的如下代码,

1
2
3
(function () {
console.log("this: " + this.name);
}());

由此可以看出,实际上进行一个匿名函数的立即执行;也就是说执行过程中并没有使用 关联调用,而是一次 standalone 函数的自身调用,所以根据法则 #4,this -> global object。执行完以后,将该匿名函数赋值给 obj.foo

再次执行,

1
2
> obj.foo();
'this: obj'

这次执行的过程是一次标准的 关联调用 过程,所以根据法则 #3,this -> obj

作为判断条件 - 匿名函数立即执行

1
2
> (false || obj.foo)() 
'windows'

等价于执行,

1
2
3
(false || function () {
console.log("this: " + this.name);
})()

原理和函数赋值变种-匿名 standalone 函数立即执行 一致,等价于立即执行如下的匿名函数

1
2
3
(function () {
console.log("this: " + this.name);
})()

其实,把这个例子再做一个细微的更改,其中逻辑就看得更清楚了,为 foo 函数添加一个返回值 return true

1
2
3
4
5
6
7
8
var name = "windows";
var obj ={
name: "obj",
foo: function () {
console.log("this: "+ this.name);
return true;
}
};

再次执行,

1
2
3
> (false || obj.foo)() 
'windows'
true

可见,obj.foo 函数执行以后,返回 true。上述代码其实等价于执行如下的代码,

1
2
3
4
(false || function () {
console.log("this: " + this.name);
return true;
})()

函数回调场景 0 - 基本原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};

function callIt(callback) {
callback();
}

> callIt(counter.inc)
TypeError: Cannot read property 'count' of undefined

可以看到,把一个定义有this关键字的函数作为其它函数的回调函数,是危险的,因为this运行期会被重新赋值,上述例子很直观的描述了这一点,之所以报错,是因为this指向了 Global Object。要解决这样的问题,可以使用bind,调用的时候改为

1
2
> callIt(counter.inc.bind(counter))
1

函数回调场景 1 - setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var name = "Bob";  
var nameObj ={
name : "Tom",
showName : function(){
console.log(this.name);
},
waitShowName : function(){
setTimeout(this.showName, 1000);
}
};

// 执行,

> nameObj.waitShowName();
'Tom'
undefined

setTimeout(this.showName, 1000);nameObj.showName 函数作为回调函数参数传递给 setTimeout;那么为什么当 setTimeout 执行回调的时候,nameObj.showName 方法返回的是 undefined 呢?为什么不是返回全局对象对应的 name Bob?原因只有一个,那就是 setTimeout 有自己的 this 对象,而它没有 name 属性,而在回调 showName 函数的时候,showName 函数中的 this 正是 setTimeout 上下文中的 this,而该 this 并没有定义 name 属性,所以这里返回 undefined

函数回调场景 2 - DOM 对象

1
2
3
4
5
6
7
var o = new Object();

o.f = function () {
console.log(this === o);
}

o.f() // true,得到期望的结果 this -> o

但是,如果将f方法指定给某个click事件,this的指向发生了改变,

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

点击按钮以后,返回的是false,是因为在执行过程中,this不再指向对象o了而改为指向了按钮的DOM对象了;Sounds Good,但问题是,怎么被改动的?看了一下 jQuery 的源码,event.js,摘录重要的片段如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function on( elem, types, selector, data, fn, one ) {
.......
if ( one === 1 ) {
origFn = fn;
fn = function( event ) {

// Can use an empty set, since event contains the info
jQuery().off( event );
return origFn.apply( this, arguments );
};

// Use same guid so caller can remove using origFn
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
}
.......
}

o.f 函数的地址赋值给 _fn_ 参数,_fn_ -> origFn,最后是通过origFn.apply( this, arguments );来调用 o.f 函数的,而这里的 this 就是当前的 DOM 对象,既是这个按钮 button;通过这样的方式,在执行过程中,通过回调函数 $(“button”).on(…) 成功的将新的 this 对象 button 注入了 o.f 函数。那么如何解决呢?参看function.prototype.apply())
的小节#3,动态绑定回调函数。

函数回调场景 3 - 数组对象方法的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
name: '张三',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name);
});
}
};

> obj.print();
'undefined'
'undefined'
'undefined'

这里我们期望的是,依次根据数组 times 的长度,输出 obj.name 三次,但是实际运行结果是,数组虽然循环了三次,但是每次输出都是 undefined,那是因为匿名函数

1
2
3
function(n){
console.log(this.name);
}

作为数组 times 的方法 forEach 的回调函数执行,在 forEach 方法内部该匿名函数必然是作为 standalone 方法执行的,所以,this指向了 Global Object

进一步,为什么“在 forEach 方法内部该匿名函数必然是作为 standalone 方法执行的”?为什么必然是作为 standalone 方法执行?是因为不能在 forEach 函数中使用 this.fn() 的方式来调用该匿名回调函数( _fn_ 作为参数引用该匿名回调函数 ),因为如果这样做,在运行时期会报错,因为在 forEach 函数的 this 对象中找不到 _fn_ 这样的属性,而该 this 对象指向的是 obj.times 数组对象。因此,得到结论“在 forEach 方法内部该匿名函数必然是作为 standalone 方法执行的”

解决办法,使用 bind

1
2
3
4
5
6
7
8
9
10
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};

> obj.print()
'张三'
'张三'
'张三'

obj 对象作为 this 绑定到该匿名函数上,然后再作为回调函数参数传递给 forEach 函数,这样,在 forEach 函数中,用 standalone 的方式调用 _fn_ 的时候,_fn_ 中的 this 指向的就是数组对象 obj 对象,这样,我们就能顺利的输出 obj.name 了。

绑定 this

有上述描述可知,this的值在运行时根据不同上下文环境有不同的值,因此我们说this的值是变化的,这就给我们的编程带来了麻烦,有时候,我们期望,得到一个固定的this。Javascript 提供了callapply以及bind这三个方法,来固定this的指向;这三个方法存储在 function.prototype 域中,

function.prototype.call()

总结起来,就是解决函数在调用的时候,如何解决this动态变化的问题。

调用格式,

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

第一个参数是在运行时用来赋值给 func 函数内部的 this 的。

通过f.call(obj)的方式调用函数,在运行时,将 obj 赋值给 this

1
2
3
4
5
6
7
8
var obj = {};

var f = function () {
return this;
};

f() === this // true
f.call(obj) === obj // true

call方法的参数是一个对象,如果参数为 _空_、null 或者 undefined,则使用默认的全局对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var n = 123;
var obj = { 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(obj)
456

如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后赋值给 this

1
2
3
4
5
6
var f = function () {
return this;
};

> f.call(5)
[Number: 5]

call方法可以接受多个参数,第一个参数就是赋值给 this 的对象,

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
name : 'obj'
}

function add(a, b) {
console.log(this.name);
return a + b;
}

> add.call(obj, 1, 2)
obj
3

call方法可以调用对象的原生方法;

1
2
3
4
5
6
7
8
9
10
var obj = {};
obj.hasOwnProperty('toString') // false

// “覆盖”掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

方法 hasOwnProperty 是对象 objObject.prototype 中继承的方法,如果一旦被覆盖,就不会得到正确的结果,那么,我们可以使用call的方式调用原生方法,将 obj 作为 this 在运行时调用,这样,变通的,我们就可以调用 obj 对象所继承的原生方法了。

function.prototype.apply()

总结起来,和call一样,就是解决函数在调用的时候,如何解决this动态变化的问题。

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

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

apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。

原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加

1
2
3
4
5
6
function f(x,y){
console.log(x+y);
}

f.call(null,1,1) // 2
f.apply(null,[1,1]) // 2
  1. 找出数组最大的元素

    1
    2
    3
    var a = [10, 2, 4, 15, 9];
    Math.max.apply(null, a)
    // 15
  2. 将数组的空元素变为 undefined

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

    空元素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
  3. 绑定回调函数的对象
    函数回调场景-2我们看到this被动态的更改为了 DOM 对象 button,这往往不是我们所期望的,所以,我们可以再次绑定回调函数来固定this,如下,

    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);

    这样,我们用 _f_ 函数封装原来的回调函数 o.f,并使用apply方法固定住this,使其永远指向 object o,这样,就达到了this不被动态修改的目的。

function.prototype.bind()

总结起来,其实就是在把函数作为参数传递的时候,如何解决this动态变化的问题。

解决的问题

在认识关联调用 - 容易混淆的场景中,我们浓墨重彩的描述了将函数赋值以后,导致this在运行期发生变化的种种场景,而且在编程过程当中,也是非常容易导致问题的场景;那么有没有这么一种机制,即便是在函数赋值后,在运行期依然能够保护并固定住我的this?答案是有的,那就是bind。下面,我们来看一个例子,

1
2
var d = new Date();
d.getTime() // 1481869925657

我们使用语句 d.getTime() 通过对象 _d_ 关联调用函数 getTime(),根据法则 #3,函数 getTime() 内部的 this 指向的是对象 _d_,然后从 _d_ 对象中成功获取到了时间。但是,我们稍加改动,将对象 _d_ 中的函数 getTime 赋值给另外一个变量,在执行呢?

1
2
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

Wow~, 画风突变,得不到时间了,而且还抛出了一个程序异常,好玩,你的程序因此崩溃.. 这就是this在执行期动态变化所导致的,当我们将函数 d.getTime 赋值给 print,然后语句 print() 表示将函数 getTime 作为 standalone 的函数在运行期调用,所以,内部的this发生变化,指向了 Global Object,也因此,我们得不到时间了,但我们得到一个意想不到的异常..

Ok, 别怕,孩子,bind登场了,

1
2
var print = d.getTime.bind(d);
print() // 148186992565

赋值过程中,将函数通过bind语法绑定this对象 _d_ 以后,再赋值给一个新的变量;这样,即便 print() 再次作为 standalone 的函数在运行期调用,this的指向也不再发生变化,而是固定的指向了对象 _d_。

bind 是如何实现的

1
2
3
4
5
6
7
8
9
10
11
if(!('bind' in Function.prototype)){
Function.prototype.bind = function(){
var fn = this; // 当前调用 bind 的当前对象 fn ( fn.bind(..) )
var context = arguments[0]; // 用来绑定 this 对象的参数
var args = Array.prototype.slice.call(arguments, 1);
var fnbound = function(){
return fn.apply(context, args);
}
return fnbound;
}
}

Function对象的prototype原型中新增一个属性bind,该bind是一个 function 函数;这里要特别特别注意,每次bind调用以后,返回的是一个新的function,

1
2
3
4
var fnbound = function(){
return fn.apply(context, args);
}
return fnbound;

通过 fnbound 函数套一层原函数 _fn_ 作为闭包,然后返回这个新的 function fnbound;大部分教程就是这样介绍即止了;其实,我想问的是,为什么bind要这么设计,直接返回fn.apply(context, args);不是挺好吗?为什么还要在外面套一层新函数 fnbound?Ok,这里我就来试图解释下原因吧;

采用反证法,如果,我们不套这么一层新函数 fubound,看看,会怎样?于是,我们得到如下的实现,

1
2
3
4
5
6
7
8
if(!('bind' in Function.prototype)){
Function.prototype.bind = function(){
var fn = this; // 当前调用 bind 的当前对象 fn ( fn.bind(..) )
var context = arguments[0]; // 用来绑定 this 对象的参数
var args = Array.prototype.slice.call(arguments, 1);
return fn.apply(context, args);
}
}

直接返回fn.apply(context, args),oh,顿时,我明白了,fn.apply(...)这是一条执行命令啊,它会立即执行 _fn_,将 _fn_ 执行的结果返回.. 而我们这里的bind的初衷只是扩充 _fn_ 函数的行为(既绑定this对象),然后返回一个函数的引用,而正式因为我们无法在绑定以后,直接返回原有函数的引用,所以,这里,我们才需要创建一个新的函数并返回这个新的函数的引用,已达到bind的设计目的。Ok,这下总算是清楚了。

特性

绑定匿名函数
1
2
3
4
5
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};

可见,我们可以直接改匿名函数执行bind,然后在将其赋值给某个对象;更详细的用例参考函数回调场景 3 - 数组对象方法的回调

作为函数直接调用
1
2
var altwrite = document.write;
altwrite("hello");

在浏览器运行这个例子,得到错误Uncaught ReferenceError: alwrite is not defined,这个错误并没有真正保留底层的原因,真正的原因是,document 对象的 write 函数再执行的时候,内部this指向了 Global Object

为了解决上述问题,我们可以bind document 对象,

1
altwrite.bind(document)("hello")

注意这里的写法,altwrite.bind(document)返回的是一个Function,所以可以直接跟参数调用。

绑定函数参数

除了绑定this对象意外,还可以绑定函数中的参数,看如下的例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
var add = function (x, y) {
return x * this.m + y * this.n;
}

var obj = {
m: 2,
n: 2
};

var newAdd = add.bind(obj, 5);

newAdd(5);
// 20

add.bind(obj, 5);除了绑定 add 函数的this对象为 obj 以外,将其固定obj 以外,还绑定了 add 函数的第一个参数 _x_,并将其固定为 _5_;这样,得到的 newAdd 函数只能接收一个参数,那就是 _y_ 了,因为 _x_ 已经被bind绑定且固定了,所以可以看到,随后执行的语句newAdd(5)传递的实际上是 _y_ 参数。

若绑定 null 或者 undefined

如果bind方法的第一个参数是 nullundefined,等于将this绑定到全局对象,函数运行时this指向 Global Object

1
2
3
4
5
6
7
8
9
10
11
12
var name = 'windows';

function add(x, y) {
console.log(this.name);
return x + y;
}

var plus = add.bind(null, 5); // 绑定了 x 参数

> plus(10) // 赋值的是 y 参数,于是执行的是 5 + 10
'windows'
15

改写原生方法的使用方式

首先,

1
2
> [1, 2, 3].push(4)
4 // 输出新增后数组的长度

等价于

1
Array.prototype.push.call([1, 2, 3], 4)

第一个参数 [1, 2, 3] 绑定 push 函数的this关键字,第二个参数 _4_,是需要被添加的值。

补充一下

为什么说这里是等价的?我们来解读一下

1
2
> [1, 2, 3].push(4)
4 // 输出新增后数组的长度

的执行过程,[1, 2, 3] 作为数组对象,调用其原型中的 Array.prototype.push 方法,很明显,采用的是关联调用,因此 push 函数内部的 this 指向的是数组对象 [1, 2, 3];而这里,我们通过

1
Array.prototype.push.call([1, 2, 3], 4)

这样的调用方式,只是换汤不换药,同样是执行的数组中的原型方法 push,只是this的传递方式不同而已,这里是通过bind直接将this赋值为数组对象 [1, 2, 3],而不是通过之前的关联调用;所以,两种调用方式是等价的。

补充完毕

再次,

call 方法调用的是 Function 对象的原型方法既 Function.prototype.call(…),那么我们再来将它 bind 一下,看看会有什么结果

1
2
3
4
5
6
7
8
9
10
11
12
> var push = Function.prototype.call.bind(Array.prototype.push);

> push([1, 2, 3], 4);
4 // 返回数组长度

// 或者写为

> var a = [1, 2, 3];
> push(a, 4);
4
> a
[1, 2, 3, 4]

我们得到了一个具备数组 push 操作的一个新的函数 push(…) ( 注: bind 每次回返回一个新的函数 );

那是为什么呢?

可以看到,背后的核心是,

1
push([1, 2, 3], 4);

等价于执行

1
Array.prototype.push.call([1, 2, 3], 4)

所以,我们得证明Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4)Array.prototype.push.call([1, 2, 3], 4)两个函数的执行过程是等价的( 注意,为什么比较的是执行过程等价,因为call函数是立即执行的,而bind返回的是一个函数引用,所以必须比较两者的执行过程 );其实,要证明这个问题,最直接方法就是去查看函数Function.prototype.call的源码,可惜,我在官网 MDN Function.prototype.call() 上面也没有看到源码;那么这里,其实可以做一些推理,

Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4)

通过bind,这里返回一个新的 call 函数,该函数绑定了 Array.prototype.push Function 对象做为其this对象;那么Function.prototype.call函数内部会怎么执行呢?我猜想应该就是执行this.apply(context, params)之类的,this表示的是 Array.prototype.pushcontext表示的既是这里的数组对象 [1, 2, 3], params表示的既是这里的参数 _4_

Array.prototype.push.call([1, 2, 3], 4)
同理,由上述Function.prototype.call函数内部的执行过程是执行this.apply(context, params)的推断来看,this依然是指向的 Array.prototype.pushcontext表示的既是这里的数组对象 [1, 2, 3], params表示的既是这里的参数 _4_;所以,这里的调用方式与 Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4) 的方式等价;所以,我们得出如下结论,
Array.prototype.push.call([1, 2, 3], 4) <=> Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4) <=> push([1, 2, 3], 4)

使用 bind 的一些注意事项

每次返回一个新函数

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

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

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

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

正确的方法是写成下面这样,使得 addremove 使用的是同一个函数的引用。

1
2
3
4
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);

use strict

使用严格模式,该部分可以参考阮一峰的教程严格模式,说得非常详细;不过应用到面向对象编程里面,主要就是为了避免this运行期动态指向 Global Object,如果发生这类的情况,报错;例如

1
2
3
4
5
6
function f() {
'use strict';
this.a = 1;
};

f();// 报错,this未定义

当执行过程中,发现函数 _f_ 中的this指向了 Global Object,则报错。

构造函数中的 this

this -> Object.prototype instance

构造函数比较特别,javascript 解析过程不同于其它普通函数;

假如我们有如下的构造函数,

1
2
3
4
var Person = function(name, age){
this.name = name;
this.age = age;
}

javascript 语法解析器解析到如下语句以后,

1
var p = new Person('张三', 35);

实际上执行的是,

1
2
3
4
5
6
7
8
9
10
11
12
function new( /* 构造函数 */ constructor, /* 构造函数参数 */ param1 ) {
// 将 arguments 对象转为数组
var args = [].slice.call(arguments);
// 取出构造函数
var constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
var context = Object.create(constructor.prototype);
// 执行构造函数
var result = constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,则返回 context 对象
return (typeof result === 'object' && result != null) ? result : context;
}

备注:arguments 可表示一个函数中所有的参数,也就是一个函数所有参数的结合。

下面,我们一步一步的来分析该构造函数的实现,弄清楚this指的是什么,

constructor

就是 Person 构造函数,

context

var context = Object.create(constructor.prototype);通过 constructor.prototype 创建了一个新的对象,也就是 Person.prototype 的一个实例 Person.prototype isntance

constructor.apply(context, args);

注意,这步非常关键,context 作为 constructor 构造函数的this,所以

1
2
3
4
var Person = function(name, age){
this.name = name;
this.age = age;
}

中的this在执行过程中指向的实际上就是该 context 对象。

result

constructor.apply(context, args);方法调用的返回值,我们当前用例中,Person 构造函数并没有返回任何东西,所以,这里是 null

return (typeof result === ‘object’ && result != null) ? result : context;

new方法的最后返回值,如果 result 不为 null,则返回 result 否则返回的是 context;我们这个用例,当初始化构造函数完成以后,返回的是 contextPerson.prototype instance,也就是构造函数中的this指针;这也是大多数构造函数应用的场景。

Object.prototype instance -> Object.prototype

1
2
3
4
5
6
7
var Obj = function (p) {
this.p = p;
};

Obj.prototype.m = function() {
return this.p;
};

执行,

1
2
3
4
5
6
7
> var o = new Obj('Hello World!');

> o.p
'Hello World!'

> o.m()
'Hello World!'

说实话,当我第一次看到这个例子的时候,o.p 还好理解,_o_ 就是表示构造函数 Obj 内部的this对象,是一个通过 Object.create(Obj.prototype) 得到的一份 Obj.prototype 的实例对象;但是,当我看到 o.m 的时候,还是有点懵逼,Obj.prototype 并不是代表的this呀,Object.create(Obj.prototype) 才是( 既 Obj.prototype instance ),所以在 Obj.prototype 上定义的 _m_ 方法,怎么可以通过 o.m() 既通过 Obj.prototype instance 来调用呢?( 注意,关系 _o_ -> Object.create(Obj.prototype) -> Obj.prototype instance -> this != Obj.prototype ) 当理解到 prototype 的涵义有,才知道,Obj.prototype instance 会继承 Obj.prototype 中的公共属性的,所以,这里通过 Obj.prototype 对象定义的 _m_ 函数可以通过 Object.prototype instance 进行调用。