谈谈对JavaScript中this、call()、apply()、bind()的理解

MDN

JavaScript this
JavaScript Function.prototype.call()
JavaScript Function.prototype.apply()
JavaScript Function.prototype.bind()

this的概念理解

this 永远指向一个对象,并且指向最后调用它的那个对象;
this 的指向完全取决于函数调用的位置;

this的指向

在绝大多数情况下,函数的调用方式决定了this的值。this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。ES5引入了bind方法来设置函数的this值,而不用考虑函数如何被调用的,ES2015 引入了支持this词法解析的箭头函数(它在闭合的执行环境内设置this的值)。(来自MDN)

全局环境(Global context)

无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 (来自MDN)

1
2
3
4
5
6
conosole.log(this === window); // true
a = 37;
console.log(window.a); // 37
this.b = 'Br3ad';
console.log(window.b); // Br3ad
console.log(b); // Br3ad

函数(运行内)环境(Function context)

在函数内部,this的值取决于函数被调用的方式。(来自MDN)

简单调用

非严格模式下:
因为下面的代码不在严格模式下,且 this 的值不是由该调用设置的,所以 this 的值默认指向全局对象(window)。

1
2
3
4
5
6
7
8
// non-strict mode
function func1 () {
return this;
};
// 在浏览器中:
func1() === window; // true
// 在node中:
func1() === global; // true

严格模式下:
this将保持他进入执行环境时的值,所以下面的this将会默认为undefined

1
2
3
4
5
6
// strict mode
function func2 () {
'use strict';
return this;
};
func2 === undefined; // true

所以,可以得出结论在严格模式下,如果 this 没有被执行环境(execution context)定义,那它将保持为 undefined

首先,来看下面一个简单的例子:

例 1:

1
2
3
4
5
6
7
8
var name = 'windowsName';
function foo () {
var name = 'Br3ad';
console.log(this.name); // windowsName
console.log('inner: ' + this); // [Object Window]
}
foo();
console.log('outer: ' + this); // [Object Window]

为什么这里console.log是 windowsName?

因为this永远指向最后调用它的那个对象”,调用foo的地方foo(),前面没有调用的对象那么就是指向全局对象 Object window,相当于window.foo()

这里没有使用严格模式,如果使用严格模式的情况下,全局对象就是undefined,那么就会报错Uncaught TypeError: Cannot read property 'name' of undefined

请看下面的例子:

例 2:

1
2
3
4
5
6
7
8
9
10
// Use Strict Mode
'use strict'
var name = 'windowsName';
function foo () {
var name = 'Br3ad';
console.log('inner: ' + this); // inner: undefined
console.log(this.name); // Uncaught TypeError: Cannot read property 'name' of undefined
}
foo();
console.log('outer: ' + this); // [Object Window]

再看下面的例子:

例 3:

1
2
3
4
5
6
7
8
var name = 'windowsName';
var bar = {
name: 'Br3ad',
fn: function () {
console.log(this.name); // Br3ad
};
};
bar.fn(); // Br3ad

在这个例子中,函数 fn 是对象 bar 调用的,所以打印的值就是 bar 中的 name 的值。

基于上面的例子,再做个改动:

例 4:

1
2
3
4
5
6
7
8
var name = 'windowsName';
var bar = {
name: 'Br3ad',
fn: function () {
console.log(this.name); // Br3ad
}
};
window.bar.fn(); // Br3ad

这里console.logBr3ad,最后调用它的对象是bar,还是因为this 永远指向最后调用它的那个对象”

再来看下面这个例子:

例 5:

1
2
3
4
5
6
7
8
var name = 'windowName';
var bar = {
// name: 'Br3ad',
fn: function () {
console.log(this.name); // undefined
}
};
window.bar.fn(); // undefined

为什么console.log会打印undefined呢?

因为,如刚刚所描述的那样,调用fn的是bar这个对象,也就是说fn内部的this是对象bar,而对象bar中并没有对name字段进行定义,所以console.logthis.name的值为undefined

这个例子还是印证了刚才的结论:this 永远指向最后调用它的那个对象,因为最后调用fn的对象是bar,所以就算bar中没有name这个属性,也不会继续向上一个对象寻找this.name,而是直接输出undefined

再来看一个复杂点的例子:

例 6:

1
2
3
4
5
6
7
8
9
var name = 'windowsName';
var bar = {
name: 'Br3ad',
fn: function () {
console.log(this.name); // windowsName
}
};
var fun = bar.fn;
fun(); // windowsName

为什么这里console.log打印出的不是Br3ad?因为虽然bar对象的fn方法赋值给了变量fun了,但是没有调用,回到之前我们的结论:“this 永远指向最后调用它的那个对象”,由于刚刚的fun并没有调用,所以fn()最后仍然是被window调用的。所以这里this指向的也就是window

以上的例子,不难发现this的指向并不是在创建的时候就可以确定的,在 es5 中,永远是:this 永远指向最后调用它的那个对象

再来看一个例子:

例 7:

1
2
3
4
5
6
7
8
9
var name = 'windowName';
function fn () {
var name = 'Br3ad';
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
};
fn(); // windowsName

怎么改变 this 的指向?

改变this的指向主要有以下几种方法:

请看下面的例子:

例子8:

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name); // this.func1 is not a function
},
func2: function () {
setTimeout(function () {
this.func1();
}, 100);
}
};
bar.func2(); // this.func1 is not a function

在不使用箭头函数的情况下,是会报错的,因为最后调用setTimeout的对象是window,但是在window中并没有func1函数。

在改变 this 指向这一节将把这个例子作为 Demo 进行改造

那么,箭头函数是如何实现的?

箭头函数(Arrow function expressions)


箭头函数的 this 始终指向函数定义时的 this,而非执行时。记住:“箭头函数没有单独的this绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,thisundefined(箭头函数会从自己的作用域链的上一层继承this)”。

请看下面的例子:

例9:

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name); // Br3ad
},
func2: function () {
setTimeout(() => {
this.func1();
}, 100);
}
};
bar.func2(); // Br3ad

在函数内部使用_that = this


如果不使用ES6,那么这种方式应该是最简单的不会出错的方式,先将调用这个函数的对象保存在变量_this中,然后在函数中都是用这个_that,这样_that就不会改变了。

请看下面的例子:

例10:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name); // Br3ad
},
func2: function () {
var _that = this;
setTimeout( function () {
_that.func1();
}, 100);
};
};
bar.func2(); // Br3ad

这个例子中,在func2中,首先设置var _that = this;,这里的this是调用func2的对象bar,为了防止在func2中的setTimeoutwindow调用而导致的在setTimeout中的thiswindow。将this(指向变量bar)赋值给一个变量_that,这样,在func2中使用_that就是指向对象bar了。

使用 apply()call()bind()

使用apply()call()bind()函数也是可以改变this的指向,先来看一下是怎么实现的:

使用apply()

来看看MDN对apply()的用法定义:

apply()方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数

语法

1
function.apply(thisArg, [argsArray])

请看下面的例子:

例11:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// function.apply()
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name); // Br3ad
},
func2: function () {
setTimeout(function () {
this.func1(); // Br3ad
}.apply(bar), 100);
}
};
bar.func2(); // Br3ad

使用call()

来看看MDN对call()的用法定义:

call()方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

语法

1
function.call(thisArg, arg1, arg2, arg3, ar4, ...);

请看下面的例子:

例子12:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// function.call()
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name); // Br3ad
},
func2: function () {
setTimeout( function (){
this.func1();
}.call(bar), 100)
}
};
bar.func2(); // Br3ad

使用bind()

来看看MDN对bind()的用法定义:

bind()方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用

语法

1
function.bind(thisArg[, arg1[, arg2[, ...]]])

请看下面的例子:

例子13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// function.bind()
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name); // Br3ad
},
func2: function () {
setTimeout(function () {
this.func1();
}.bind(bar)(), 100);
}
};
bar.func2(); // Br3ad

JavaScript 中 call()apply()bind()的区别?

现在,我们都知道使用call()apply()bind()函数都可以改变JavaScript中this的指向,但是这三个函数稍有不同。

MDN中定义apply()如下:

apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数

语法

1
function.apply(thisArg, [argsArray])

参数:

thisArg(必须) 在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

argsArray(可选) 一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 nullundefined,则表示不需要传入任何参数。从ECMAScript 5开始可以使用类数组对象。

apply()call() 的区别

apply()call() 基本类似,他们的区别只是传入的参数不同

call()的语法为:

1
function.call(thisArg, arg1, arg2, ...)

apply()call()的区别就是:

call()方法接受的是若干个参数列表,而apply()接收的是一个包含多个参数的数组。

请看下面的例子:

例子14:

1
2
3
4
5
6
7
8
9
10
// function.apply()
var name = 'windowsName';
var bar = {
name: 'Br3ad',
fn: function (a, b) {
console.log(a + b); // 3
}
};
var b = bar.fn;
b.apply(bar, [1, 2]); // 3

例子15:

1
2
3
4
5
6
7
8
9
// function.call()
var bar = {
name: 'Br3ad',
fn: function (a, b) {
console.log(a + b); // 3
}
};
var b = bar.fn;
b.call(bar, 1, 2); // 3

bind()apply()call()的区别

现在,将刚刚的例子使用bind()试一下

1
2
3
4
5
6
7
8
9
// function.bind()()
var bar = {
name: 'Br3ad',
fn: function (a, b) {
console.log(a + b);
}
};
var b = bar.fn;
b.bind(bar, 1, 2); // 到这一步并没有输出,这是因为bind()方法创建了一个新函数,需要进一步调用才能执行

会发现并没有输出,这是为什么呢,我们来看一下 MDN 上的文档说明:

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

1
2
3
4
5
6
7
8
9
// function
var bar = {
name: 'Br3ad',
fn: function (a, b) {
console.log(a + b); // 3
}
};
var b = bar.fn; // 到这一步并没有输出,这是因为bind()方法创建了一个新函数,需要进一步调用才能执行
b.bind(bar, 1, 2)(); // 调用bind()方法创建的新函数,并正确输出了结果:3

JavaScript 中的函数调用

例7:

1
2
3
4
5
6
7
8
9
10
var name = 'windowsName';
function fn () {
var name = 'Br3ad';
innerFunction();
function innerFunction () {
console.log(this.name); // windowsName
}
};

fn();

例8:

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name);
},
func2: function () {
setTimeout( function(){
this.func1();
}, 100)
}
};
bar.func2()

函数调用的方法一共有 4 种

  • 作为一个函数调用
  • 函数作为方法调用
  • 使用构造函数调用函数
  • 作为函数方法调用函数(call()apply()

作为一个函数调用

比如上面的例子1:

例1:

1
2
3
4
5
6
7
8
9
// non-strict mode
var name = 'windowsName';
function foo () {
var name = 'Br3ad';
console.log(this.name); // windowsName
console.log('inner:' + this); // inner: [object Window]
};
foo();
console.log('outer:' + this); // outer: [object Window]

这是一个简单的函数,在浏览器运行环境中的非严格模式(non-strict mode)默认是属于全局对象 window 的,在严格模式(strict mode),就是 undefined这是一个全局的函数,很容易产生命名冲突,不建议这样使用。

1
2
3
4
5
6
7
// strict mode
'use strict';
var name = 'windowsName';
function foo () {
var name = 'Br3ad';
console.log(this.name);
}

函数作为方法调用

使用构造函数调用函数

作为函数方法调用函数

JavaScript 中,函数是对象
JavaScript 函数有它的属性和方法。
call()apply()是预定义的函数方法。两个方法可用于调用函数,两个方法的第一个参数必须是对象本身
JavaScript 严格模式(strict mode)下,在调用函数时第一个参数会成为this的值,即使该参数不是一个对象
JavaScript 非严格模式(non-strict mode)下,如果第一个参数的值是nullundefined,它将使用全局对象替代。

再来看例子6:

1
2
3
4
5
6
7
8
9
var name = 'windowsName';
function fn () {
var name = 'Br3ad';
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
};
};
fn();

这里的innerFunction()的调用属于第一种调用方式:作为一个函数调用(作为一个函数调用、没有挂载在任何对象上,所以对于没有挂载在任何对象的函数,在非严格模式(non-strict mode)下就是指向window的)

然后再看一下例7:

例7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = 'windowsName';
var bar = {
name: 'Br3ad',
func1: function () {
console.log(this.name);
},
func2: function () {
setTimeout(function (){
this.func1()
}, 100)
}
};

bar.func2(); // this.func1 is not a function

得出结论,可以简单理解为:匿名函数的this永远指向window

在这之前,我们得出结论:this永远指向最后调用它的那个对象,那么去找最后调用匿名函数的对象,
但是因为匿名函数没有名字,所以没有办法被其他对象调用匿名函数的。所以:匿名函数的 this 永远指向 window

那么问题来了,匿名函数是如何被定义的?匿名函数是自执行的,就是在匿名函数后面加()让其自执行。其次就是虽然匿名函数不能被其他对象调用,但是可以被其他函数调用,比如例7中的setTimeout


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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
`This` 是在你调用一个函数,但尚未执行函数内部代码前被指定。(查看参考链接中的执行环境的文章,这个阶段,实际就是初始化变量对象,在初始化变量对象的时候,确定了`this`的指向)实际上,`this`是 被调用的函数的父作用域 提供的

+ 影响`this`的指向:
+ 对象中的方法
+ 事件绑定
+ 构造函数
+ 定时器
+ 函数对象的`call()`、`apply()`



this查找引用:

隐式绑定
显式绑定
new 绑定
window绑定

### 隐式绑定

### 显式绑定

## `call()`

> “call” 是每个函数都有的一个方法,它允许你在调用函数时为函数指定上下文。


## `apply()`

## `bind()`

### new 绑定

### window绑定

如果其它规则都没满足,JavaScript就会默认 this 指向 window 对象。

> *在 ES5 添加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 保持为 undefined。*

```javascript
'use strict'
window.age = 27;
function sayAge () {
console.log(`My age is ${this.age}`);
}
sayAge(); // TypeError: Cannot read property 'age' of undefined

严格模式

在严格版中的默认的this不再是window,而是undefined。

1、查看函数在哪被调用
2、点左侧有没有对象?如果有,它就是 “this” 的引用。如果没有,继续往下。
3、该函数是不是用 “call”、“apply” 或者 “bind” 调用的?如果是,它会显式地指明 “this” 的引用。如果没有,继续往下。
4、该函数是不是用 “new” 调用的?如果是,“this” 指向的就是 JavaScript 解释器新创建的对象。如果没有,继续往下。
5、是否在“严格模式”下?如果是,“this” 就是 undefined,如果不是
6、JavaScript,“this” 会指向 “window” 对象

参考链接:

稀土掘金-this、apply、call、bind
阮一峰-JavaScript 的 this 原理
javascript this指向
How to use the apply(), call(), and bind() methods in JavaScript
Understanding This, Bind, Call, and Apply in JavaScript
Function.prototype.apply()
Function.prototype.call()
Function.prototype.bind()
理解 JavaScript 中的 this、call、apply 和 bind
Understanding the “this” keyword, call, apply, and bind in JavaScript
JavaScript 之 this 指南
javascript 基础之 call, apply, bind
JavaScript中的call、apply、bind深入理解
彻底弄清 this call apply bind 以及原生实现
如何在 JavaScript 中使用 apply(),call(),bind()
面试官问:能否模拟实现JS的bind方法