【深入JS】彻底弄懂JS函数调用和this指向

537 阅读8分钟

在开始研究JS的函数调用方式和this指向问题之前,我们可以先看一段代码。

function Func () {
	this.a = function () {
		console.log(1)
	}
	Func.a = function () {
		console.log(2)
	}
}
Func.prototype.a = function () {
	console.log(3)
}
Func.a = function () {
	console.log(4)
}

Func.a()
let obj  = new Func()
obj.a()
Func.a()

这段代码的执行结果是 4、1、2。接下来会一步一步地分析出,为什么这段代码的执行结果4、1、2。

1. 函数是一等公民

函数是一等公民,或者说函数是第一类对象的意思是:

JS中函数拥有对象的所有能力,也因此函数可被最为任意其他类型对象来对待。

函数能:

  • 通过字面量创建。let f = function (){ /* something */}
  • 赋值给其他变量,数组项或其他对象的属性。同时也具有动态创建和分配属性的能力。
  • 作为函数的参数,或者返回值来传递。

由于函数是一等公民,在声明函数(声明式,通过字面量或者函数构造器创建)时,都生成了一个对象。文章开头的代码块中,执行到第一个函数调用(16行)时,本质时调用了Func这个对象的a属性方法。而在13行处,为Func这个属性挂载了a属性方法。这里会产生疑惑的是,在第五行同样为Func挂载了a属性,为什么第一个打印的不是2呢?因为Func作为一个函数,本身并没有被调用,所以第5行所在的函数内的代码块并没有被执行。在调用Func.a时,执行的方法就应该是第13行声明的函数,打印结果4。

函数是一等公民是函数式编程中的重要概念。从这个特性可以延伸出很多有趣的玩法。比如回调函数,存储函数,函数缓存。

2. 函数调用方式

函数的调用方式对函数内代码的执行有很大的影响。主要是体现在this指向和以及函数上下文是如何建立起来的。

函数的调用方式大致有下面四种:

  • 作为一个函数被直接调用。
  • 作为一个方法,关联在一个对象上调用。(可实现面向对象思想)
  • 作为一个构造函数被调用。
  • 通过函数的apply、call或bind方法调用。

2.1 作为函数被直接调用

在这种情况下,this的指向较为简单直接,在浏览器环境中,指向window或者undiluted,取决于是否处于严格模式。

2.2 作为一个方法,关联在一个对象上调用

函数在被调用的时候,会隐式的传递一个this参数。函数作为方法被调用时,父级对象会作为this传递进方法进行调用。利用这个特性可以实现存储函数。 如果我们要实现一个功能,可以随时添加一个对象到一个列表中。但是有可能同一个对象会被重复添加,这里并不希望被重复添加。当然也可以每次添加的时候循环整个数组判断。也可以利用面向对象的思想,实现一个具有数据和行为的对象,即上文中提到的存储函数。

let store = {
	nextId : 1,
	cache: {},
	add: function (obj) {
		if(!obj._store_id) {
			obj._store_id= this.nextId++
			this.cache[obj._store_id] = obj
			return true
		}
	}
}
let a = {name: test}
store.add(a)

add函数作为store的方法被调用,在方法中的this指向即store对象。这样做实现了对行为和数据的封装,避免了变量名的全局污染。但是也有不好的地方就是:store不是一个可以任意实例化的对象。

!! 注意这里的add方法不能使用箭头函数来定义,具体的原因在后文的箭头函数中阐述。

以上是比较简单的一种情形。想象一下如果有一个对象obj,obj有一个属性方法getThis,返回this,也就是obj。然后在定义一个对象obj2,通用希望有一个个obj一样的getThis方法,为了避免重复代码,定义时直接复用了obj.getThis方法。

let obj = {
	getThis: function () {
		return this
	}
}
let obj2 = {
	getThis: obj.getThis
}

调用obj.getThis()毫无疑问返回的是obj对象。但是调用obj2.getThis()呢? 答案时obj2。这个答案让人十分欣慰,因为这样,我们可以合理地复用一些定义。 这里也体现了匿名函数this的指向问题:指向被调用时的上下文。也就是说,this时在被调用时才动态确定,将被挂载的父级对象作为this,隐式的传递进函数。我们大可以在不同的地方创建函数,然后在对象创建时,为属性方法赋值(不要使用箭头函数创建!!)。

2.3 作为构造函数调用

作为构造函数使用时,需要配合new关键字使用。

let obj = new Object

要研究作为构造函数使用时,哪些过程涉及this指向,我们可以先弄清楚:在使用new关键字时,js究竟干了什么。

  • 构造一个空对象,并作为this参数,传递给构造函数,作为构造函数的上下文。
  • 构造函数执行完毕,这个对象作为new语句的返回值(除了构造函数有返回值且是一个对象除外)。
  • 将这个对象的__proto__指向构造函数的原型prototype(关于这一点的拓展可以看另一篇文章——理解__proto__ 和 prototype 是什么关系)。

回到开头的代码中。第17行,使用new关键字,进行了构造函数的调用,生成了实例对象obj。在构造函数中对this添加了a方法,打印出1。这个地方还有点值得讨论的是,在第9行对Func的原型对象添加了a方法。Js在属性查找时,先查找对象本身的属性,再沿着原型链对每一个节点进行查找。所以,如果在构造函数中不对this添加加a方法,第二次将会调用到原型上的对应方法。所以第二次对a调用,输出是1。

在构造函数中,还对自己本身Func.a的方法进行了更改,所以第三次对a调用,输出是2。

注意:

  • 如果构造函数返回一个对象,则该对象将作为整个表达式的值返回。而传入构造函数的this将被丢弃。
  • 但是,如果构造函数的返回值是非对象类型,则忽略返回值,返回新创建的对象。

2.4 使用call、apply方法调用

如果想要显示地指定一个函数执行的上下文,可以使用call、apply方法调用。这两个方法是JS函数类型Function的内置方法。调用方式并不复杂,没啥好说的。

let juggle= function (...nums) {
	return this.total += nums.reduce((total, num) => total + num, 0)
}
let counter = {total: 0}
juggle.call(counter, 1,2,3)
juggle.apply(counter, [1,2,3])

apply()call()的区别就是调用参数前者以数组的方式传入,后者是参数列表的方式。下面是apply/call的模拟实现。

Function.prototype.myCall = function (context, ...args) {
	// 避免属性名冲突
	let prop= getHashCode(this.toString())
	context[prop] = this
	let result = context[hashStr](...args)
	delete context[hashStr]
	return result
}

3. bind 方法 和 箭头函数

3.1 bind方法

bind方法也是JS函数类型Function的内置方法。

let juggle2Counter  = juggle.bind(counter)

所有的函数均可以访问bind方法,可以创建并返回一个新函数,并绑定在传入的对象上。

在创建返回函数内,this指向被绑定的对象。调用bind方法不会修改原始函数,而是创建了一个新函数。

3.2 箭头函数

箭头函数是ES6中新增的语法。前面提到匿名函数或者普通函数,在调用时都会隐式地传入thisarguments两个参数,但是箭头函数不会。

箭头函数的this与声明所在的上下文相同。

这个特性能让我们绕开很多问题。比如当一个函数作为回调函数传入其他代码块执行时,箭头函数内部的this能保证访问的是我们定义的时候想要的this。但是如果过于大意,也会有其他的问题。

let obj = {
	getThis: () => this
}
assert(obj.getThis() == obj,'ok')

如果上述代码的getThis使用匿名函数定义那么毫无问题,this在调用时确定,上下文是obj。但是使用箭头函数,在定义时就确定了。在对象初始化时,this的指向是window/undefined。也就是说,当我们通过定义对象的方式实现面向对象或者代码封装时,不要使用箭头函数定义属性方法。在vue2.x中,methods内的属性方法中,如果希望this指向当前vue组件的实例,不能使用箭头函数就是这个原因。因为vue2.x中是使用对象的方式定义组件的。

以上就是关于JS函数调用的相关内容和this指向的一些细节。大部分参照《JavaScript忍者秘籍》第四章以及MDN。欢迎指正。