this指向你真的搞懂了吗?

417 阅读8分钟

虽然this指向有一句广为流传的话”谁调用就指向谁“,但却并不全面,在复杂的使用场景中,this指向容易被混淆,带出隐藏bug。this指向一直是一个基础但是最重要的概念。

本文是侯策大大《前端开发基础知识进阶》一文的读书笔记整理 + 补充了部分知识点。

PS: 侯策大大的书写的非常好,读完后对整个this指向的理解清晰多了,读侯策大大的书,有一种融会贯通的感觉,也推荐大家阅读。

死记硬背规律

  1. 在函数体中,非显式或隐式地(通过obj.fn()对象上方法这种方式叫做隐式绑定)简单调用函数时,在严格模式下,函数内的 this 会被绑定到undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。

  2. 隐式绑定:一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。

注意: 当以变量方式取出对象方法,再去调用的时候,调用的时候是没有绑定对象的,隐式绑定丢失,这种情况下this指向参考1

  1. 显示绑定:一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。

  2. 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。

  3. 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。 箭头函数的this指向在定义时就确定了,没法改变,new、显示绑定也不能改变

当然,真实环境多种多样,下面就根据具体环境来逐一梳理。

一. 全局环境中的this

例子1


// ex1

function f1(){
	console.log(this)
}


function f2(){
	'use strict'
	console.log(this)
}

f1() // window
f2() //undefiened

容易混淆的例子2


let foo = {
	'name': 'haha',
	bar: function(){
		console.log(this)
		console.log(this.name)
	}
}

let fn = foo.bar
fn()

输出结果与分析

fn()  
// window
// undefiened

这里this指向了window,因为bar虽然是对象的方法,但在复制给变量fn后。fn的执行是在window的全局环境中执行的,在非严格模式下,this指向window.

例子2 变种

let foo = {
	'name': 'haha',
	bar: function(){
		console.log(this)
		console.log(this.name)
	}
}

foo.bar()

输出结果与分析

foo.bar()
// foo
// 'haha'

this指向最后调用它的对象。在foo.fn(),fn被foo调用,this指向了foo对象。

二. 上下文对象对用中的this

案例

const o1 ={
	text:'ol'
	fn: function(){
		return this.text
	}
}

const o2 ={
	text:'o2'
	fn: function(){
		return o1.fn()
	}
}

const o3 ={
	text:'o3'
	fn: function(){
		var fn = o1.fn
		return fn()
	}
}

o1.fn()
o2.fn()
o3.fn()

输出结果与分析

o1.fn()  // 'o1'
o2.fn() //  'o1'
o3.fn()	// 'undefiened'

第2条,因为fn被调用的时候,调用的对象显示的绑定了o1.所以输出了 'o1' 第3条,先把o1.fn复制给fn。然后fn被调用,被调用时fn没有显示绑定,所以this指向全局对象。

案例进阶

如果希望让 console.log(o2.fn())语句输出o2, how to do ?

    1. bind、call、apply 改变this指向
    1. 如果不用bind、call、apply呢? => 简单改写函数
	const o2 = {
		text: 'o2',
		fn: o1.fn
	}

	console.log(o2.fn()) 

可以通过提前进行复制操作,将函数fn挂在到o2对象上,fn最终作为o2对象的方法被调用。

三. 通过bind、apply、call改变this指向

1. 区别

call => 直接调用函数,接受的参数是 (context,接受多个参数作为调用函数的入参) apply => 直接调用函数,接受的参数是 (context,参数数组) bind => 不执行,返回新函数,新函数的this绑定传入的context (context,接受多个参数作为调用函数的入参)。仍然可以接受参数列表,在新函数被调用时会传入

代码调用示例,把fn的this指向target对象

const target = {}

// call

fn.call( target, 'arg1','arg2')

// apply
fn.apply (target, ['arg1','arg2'])

// bind 
fn.bind( target, 'arg1','arg2')

这里this的指向也很容易明白,不过三、四章内容一般在知识体系上考察的重点在于明白这几个方法的原理,如何手写实现,这部分内容可参见手写相关方法

四. 构造函数(new)和this

1. 简易demo

	function Foo(){
		this.bar = "hhh"
	}

	const instance = new Foo()
	console.log(instance.bar)

结果:

	console.log(instance.bar) // 'hhh'

2.进阶

场景1:

	function Foo(){
		this.bar = "hhh"
		const o = {}
		return o
	}

	const instance = new Foo()
	console.log(instance.bar)

场景2:

	function Foo(){
		this.bar = "hhh"
		return 'haha'
	}

	const instance = new Foo()
	console.log(instance.bar)

结果与分析

	// 场景1输出 'undefiened'
	// 场景2输出 ‘hhh’

这里场景1输出'undefiened'、场景2输出‘hhh’。是因为1中new Foo()返回的是一个空对象,2中返回的是目标对象实例。

这里涉及到构造new实现的原理: 如果构造函数中显式返回一个值,且返回的是一个对象 (返回复杂类型),那么 this 就指向这个返回的对象:如果返回的不是一个对象(返回基本类型),那么 this 仍然指向实例。更进一步的代理讲解可查看下面章节手写相关方法

五. 箭头函数


const foo = {
	fn: function(){
		setTimeout(function(){
			console.log(this)
		})
	}
}
console.log(foo.fn())

分析: this是在setTimeout中以匿名函数中,同时调用的时候是没有绑定上下文的,所以输出 window

const foo = {
	fn: function(){
		setTimeout(()=> {
			console.log(this)
		})
	}
}

分析: this是在箭头函数中,this指向定义时的环境,即fn这个局部作用域。

箭头函数的this比较简单易懂,但是其需要注意的是,结合this优秀级,在复杂情况下判断this指向。

六. this优先级

  1. 显示绑定 > 隐式绑定 这个很容易理解
  2. new绑定 > 显示绑定
  3. 箭头函数this > 其他,不能被修改

重点举例子分析2、3点。

案例1

	function foo(a){
		this.a = a
	}

	const obj1 = {}

	var bar = foo.bind(obj1)
	bar(2)
	console.log(obj1.a)

	var baz = new bar(3)
	console.log(baz.a)

结果与分析

console.log(obj1.a) // 2 
console.log(obj1.a) // 3  => new的优先级高于bind,修改了this的指向

案例2

function foo(){
	return a => {
		console.log(this.a)
	}
}

const obj1 = {
	a: 2
}

const obj2 = {
	a: 3
}

const bar = foo.call(obj1)
console.log(bar.call(obj2))

结果与分析

console.log(bar.call(obj2)) // 输出2

// 1. foo.call(obj1) 把foo的this指向了obj1,同时返回了this指向为obj1的bar(引用箭头函数)。bar中的箭头函数的指向是bar (修改的是箭头函数所在的环境的this指向)
// 2. bar.call(obj2) 修改箭头函数this指向,不生效

进阶,把a改成用const声明

const a = 123

const foo = () => a => {
	console.log(this.a)
}
const obj1 = {
	a: 2
}

const obj2 = {
	a: 3
}

const bar = foo.call(obj1)
console.log(bar.call(obj2))

结果与分析

console.log(bar.call(obj2)) // 输出undefiened

答案为undefiened,因为const声明的变量不会挂载到window全局对象上。

七. 手写相关方法

call、apply、bind核心思路: 将函数挂载到指定对象的属性上, 然后再通过对应的对象调用函数

1. call

Function.prototype.myCall = function (context, ...args) {
    if (typeof this !== 'function') {
        throw new Error("this must be a function");
    }
    context = context || window || global;
	// 使用symbol,防止属性名污染
    const fn = Symbol();
 	// call 调用模式为函数自己调用call, 其中this 指向含函数本身 
    context[fn] = this;
    const result = context[fn](...args);
    delete context.fn;
    return result;
}

2. apply


Function.prototype.myApply = function (context, args) {
    if (typeof this !== 'function') {
        throw new Error("this must be a function");
    }    
	context = context || window || global;
    const fn = Symbol();
    context[fn] = this;
    const result = context[fn](...args);
    delete context.fn;
    return result;
}

3. bind

bind要区分两种场景

  1. 被当做构造函数,通过new操作符调用

=> 通过New操作符调用,不绑定传入的this,而是把this指向实例化出来的对象

  1. 作为普通函数调用

=> 作为普通函数调用,直接改变this指向即可。

/** 返回绑定this的函数 */
Function.prototype.myBind = function(context, ...args) {
    if (typeof this !== 'function') {
        throw new Error("this must be a function");
    }
    context = context || window || global;
    const fn = Symbol();
    const _this = this;
    // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,
    // 通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
    // 此时由于new操作符作用  this指向result实例对象  而result又继承自传入的_this 根据原型链知识可得出以下结论
    function newFunction (...innerArgs) {
        // 第一种情况newFunction 被new 调用,即被作为构造函数被调用
        if (this instanceof _this) { 
			// 也可以通过 new.target 来判断, 在普通函数中new.target 为null 展示
            this[fn] = _this;
            const result = this[fn](...[...args, ...innerArgs]);
            delete this[fn];
            return result;
        } else {
             context[fn] = _this;
             // 如果只是作为普通函数调用  那就很简单了 直接改变this指向为传入的context
             const result = context[fn](...[...args, ...innerArgs]);
             // delete context[fn];
             return result;
        }
    }
    /** bind 函数绑定原函数的原型 */
    newFunction.prototype = Object.create(this.prototype);
    return newFunction;
}

4. new

new的两个核心:

  1. new + 构造函数得到的实例可以实现对构造函数this属性的继承
  2. new + 构造函数得到的实例可以访问到构造函数的原型

new的原理:

  1. 创建一个空对象
  2. 将空对象的原型对象指向构造函数的原型属性,从而继承原型的方法
  3. 将构造函数的this指向绑定到空对象上执行,以获取私有属性
  4. 如果构造函数返回了一个对象,就直接返回这个对象,如果返回的不是对象,就把创建的对象返回
function myNew(Func,...args){
    let obj = {};
    //  ES6写法,建议不适用__proto__,使用setPrototypeOf
    Object.setPrototypeOf(obj,Func.prototype)
    let res = Func.call(obj,...args)

    return typeof res === 'object' ? res : obj;
}

参考文章

<<前端开发基础知识进阶>>第一章

【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)