JavaScript中的this

JavaScript中的函数,除了在声明时定义的形式参数,还会有两个附件参数:thisarguments。用过面向对象编程的语言对this应该不会陌生,但是JavaScript里this到底指向什么,取决于函数调用的模式。在JavaScript中一共有4中调用模式:方法调用模式、函数调用模式、构造器调用模式、apply(或call)调用模式。在这些模式中this指向各不相同。此外,ES6新增的特性:箭头函数表达式,与普通函数对this的指向处理也不同。

方法调用模式

当一个函数被保存为对象的一个属性时,我们称它为一个方法。方法中的this指向的是它所属的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {
value: 0,
increment: function (inc) {
// 此处的 this 指向的是 myObject,所以在该方法中能通过 this 访问到 value
this.value += typeof inc === 'number' ? inc : 1
}
}

// myObject.value 变为 1
myObject.increment()

// myObject.value 变为 3
myObject.increment(2)

this是在运行时才绑定到方法所属的对象上,而不是在定义对象时。这使得如果myObject动态修改了其他属性,this依然能访问到,极大提高了this的复用度。

函数调用模式

当一个函数并非一个对象的属性时,那么它就是当一个函数来调用的。函数中的this会指向一个全局对象,在浏览器中,这个全局对象就是window

1
2
3
4
var add = function (x, y) {
// 打印这里的 this 可以看到,就是当前的 window 对象
return x + y
}

这里有一个容易让人搞错的地方,对于内部函数,它的this也同样指向了全局对象,这就导致了一个方法里的内部函数无法访问到这个方法所属的对象的内容。Douglas Crockford认为这是JavaScript中一个设计错误。所以有时我们会看到var that = this这样的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
myObject.double = function () {
var that = this
var helper = function () {
// 打印这里的 this 可以看到,是当前的 window 对象

//that = this 这样方式,让 helper 能访问 myObject 中的属性

that.value = add(that.value, that.value)
}

helper()
}

myObject.double()

构造器调用模式

JavaScript里是没有真正的类的,它是基于原型继承的语言,new 运算符让它看起来很像Java这种基于类的语言风格,但 new 运算符背地里做的事与基于类的语言却完全不同。如果在一个函数前面带上 new 来调用,那么背地里将会创建一个连接到该函数的prototype成员的对象,同时 this 会绑定到那个新对象。

1
2
3
4
5
6
7
8
9
10
11
12
var Quo = function (string) {
this.status = string
}

Quo.prototype.get_status = function () {
return this.status
}

var myQuo = new Quo('confused')

// get_status 中的 this 此时指向 myQuo
myQuo.get_status()

这里有两个地方要注意,一个是如果一个函数在声明时希望别人以 new 运算符的方式调用,那名字的首字母一定要记得以大写字母开头,以明确告知别人。否则直接使用一个构造器函数会得到意想不到的结果,并且编译时没有警告,运行时也没有警告。调用构造器函数时不在前面加上new的话,就变成了以函数模式调用,那函数里面的this就会指向全局对象,Quo中的status就会被绑定到window上。

还有一个是如果构造器函数显示返回了一个object类型的对象,那么new调用构造器时,最终返回的就是这个对象了,而不是我们之前期待的this

1
2
3
4
5
6
7
8
9
var MyClass = function(){
this.name = 'sven'
return { // 显式地返回一个对象
name: 'anne'
}
}

var obj = new MyClass()
console.log ( obj.name ); // 打印 anne

apply(或call)调用模式

这一部分主要搞清楚applycall到底用是干嘛用的的,以及它们实际会去解决什么问题。

applycall是定义在Function原型上的两个方法:Function.prototype.applyFunction.prototype.call。记住这一点,所以我们可以在任何函数上调用applycallapplycall的作用一模一样,都是来执行调用它们的函数,他们的第一个参数都是指定函数体内this的指向。不同的是第二个参数,apply接受一个带下标的集合,这个集合可以是数组也可以是类数组。call传入的参数不固定,从第二参数开始,依次传入每个参数。

1
2
3
4
5
6
7
var func = function (a, b, c) {
console.log(a, b, c)
}

func.apply(null, [1, 2, 3]) // 打印 1, 2, 3

func.call(null, 1, 2, 3) // 打印 1, 2, 3

当明确知道要执行的函数传入哪些参数时,可以使用call,使用apply也是可以的,其他情况都可以使用applyapplycall有着更高的使用率,call更像是包装在apply上面的语法糖。

上面的例子中第一个参数都传入了null,此时func内的this指向的是全局对象,在浏览器中则是window。如果在严格模式下,this就是null。有时我们使用applycall的目的不在于指定this指向,而是另有他途,比如借用其他对象的方法,那么我们可以传入null

1
Math.max.apply(null, [1, 2, 5, 3])

下面来看一下applycall的用途。

改变this指向

callapply最常见的用途是改变函数内部的this指向,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj1 = {
name: 'Jone'
}

var obj2 = {
name: 'Candy'
}

window.name = 'window'

var getName = function () {
console.log(this.name)
}

getName() // 打印 window
getName.call(obj1) // 打印 Jone
getName.call(obj2) // 打印 Candy

当执行getName.call(obj1)这句代码时,getName函数体内的this就指向obj1了,所以实际相当于:

1
2
3
var getName = function () {
console.log(obj1.name)
}

还记得在函数调用模式时我们用var that = this解决的那个问题,也可以用callapply来解决。

1
2
3
4
5
6
7
8
9
10
11
myObject.double = function () {

var helper = function () {
this.value = add(this.value, this.value)
}

helper.call(this)
// 这个时候 helper 函数内的 this 就指向了 myObject对象
}

myObject.double()

借用其他对象的方法

借用方法的第一种场景是“借用构造函数”,通过这种技术,可以达到一些类似继承的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var A = function (name) {
this.name = name
}

var B = function () {
A.apply(this, arguments)
}

B.prototype.getName = function () {
return this.name
}

var b = new B('Jone')
console.log(b.getName()) // 打印 Jone

B现在有了name属性,但是B不会拥有A原型上的方法和属性,所以只有B想有A构造器中的一些属性时再用apply这样来做。

下面看一个用apply借用其他对象的方法更加实用的例子,函数的参数列表arguments,虽然有下标,但是它不是数组,它只是类数组,所以本身不具有Array.prototype中的方法,但很多时候我们又需要它能像数组一样对它进行操作。这时就可以用apply来借用Array.prototype中的方法。

1
2
3
4
5
var fn = function () {
var arr = Array.prototype.slice.apply(arguments)
console.log(arr) // [ 1, 2 ]
}
fn(1, 2)

这一点在平时的开发中还是很实用的,一定要掌握。但也不是所有类型的数据都可以借用Array.prototype的,必须要满足两个条件:

  • 对象本身要可以存取属性;
  • 对象的length属性可读写。

想要了解具体的原因,可以查阅《JavaScript设计模式与开发实践》第 53 页。

箭头函数

再回到介绍函数调用模式时,提到的一个问题,内部函数内的this无法跟外部函数内的this指向同一个对象,导致内部函数无法访问对象的属性。可以用var that = this;来解决,也可以用applycall来改变this指向。

不管怎样解决,有时都需要开发人员反应一会才能明白,尤其看别人代码的时候。好在ES6推出了箭头函数,箭头函数在写法上比普通函数要简洁,更加接近数学表达式,有时也管它叫lambda表达式。JavaScript中的箭头函数没有单独的this,如果在它内部使用了this,那这个this的指向跟定义它的位置的this指向一致。

将上面的helper函数改为箭头函数之后,value就可以正确访问到了:

1
2
3
4
5
6
7
8
9
10
myObject.double = function () {

var helper = () => {
this.value = add(this.value, this.value)
}

helper()
}

myObject.double()

此外,因为箭头函数没有单独的this参数,如果对箭头函数调用applycall,第一个参数可以省略不传,即使传了也会被忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var adder = {
base: 1,
add: function (a) {
var f = v => v + this.base
return f(a)
},
addThruCall: function (a) {
var f = v => v + this.base
var obj = {
base: 2
}
return f.call(obj, a)
}
}

console.log(adder.add(1)) // 打印 2
console.log(adder.addThruCall(1)) // 还是打印 2,忽略了 obj

相比于普通函数,箭头函数还有其他一些特性:

  • 箭头函数不会绑定arguments参数;
  • 箭头函数没有prototype属性;
  • 箭头函数不能用作构造器,和new一起用会抛出错误:TypeError: xx is not a constructor
  • yield关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。

要了解具体介绍,可查阅MDN-箭头函数一节。

参考资料

【1】 《JavaScript语言精粹》

【2】 《JavaScript设计模式与开发实践》

【3】 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions

本文作者:意林
本文链接:http://shinancao.cn/2019/07/28/JS-this/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!