如何理解JavaScript里的this

JavaScript里的this

0️⃣什么是this?

this可以表示JavaScript中函数的运行环境,也叫上下文(context);可以简单理解为this表示是谁在调用这个function(为了避免混淆,这里指用function关键字定义的代码,下同)。

在JavaScript中function关键字定义的代码的触发方式可以分为三种:

  • 方法调用(Method Invocation)
  • 函数调用(Function Invocation)
  • 构造器调用(Constructor Invocation)

不同的种类,代码中的this关键字指向是不同的。

1️⃣Method Invocation里的this

当在JavaScript对象里使用function定义对象的属性(property)或者域(field)时,function表示方法(method)。

Method Invocation指的是类似于object.methodName(arg)的调用方式,或者是(expression)(arg)的调用方式。这里的expression是JavaScript里的表达式,当这个表达式最后的结果是个object时,调用方式依然是Method Invocation。

Method Invocation里的this被绑定到了调用时的object,例如:

var object = {
	methodName: function () {
		console.log(this);
	}
};
object.methodName();	// {methodName: ƒ}

2️⃣Function Invocation里的this

Function Invocation指的是类似于functionName(arg)的调用方式,或者是(expression)(arg)的调用方式。这里的expression是JavaScript里的表达式,当这个表达式最后的结果是个function时,调用方式依然是Function Invocation,即使表达式里包含形式与Method Invocation很相似的object.methodName

Function Invocation里的this被绑定到了全局的Window对象上,例如:

var object = {
	methodName: function () {
		console.log(this);
	}
};
(object.methodName = object.methodName)();	// Window
(false || object.methodName)();				// Window
// 逗号运算符:对两个表达式求值并返回后一个表达式的值
(1, object.methodName)();					// Window

注意在严格模式下,Function Invocation里的this默认不再绑定到Window对象上,而是undefined使用 'use strict' 进入严格模式

'use strict'
var object = {
	methodName: function () {
		console.log(this);
	}
};
(object.methodName = object.methodName)();	// undefined
(false || object.methodName)();				// undefined
(1, object.methodName)();					// undefined

3️⃣Constructor Invocation里的this

在JavaScript里的面向对象编程:

function ClassName(name) {
	var age;			// 私有域
	this.name = name;	// 公有域
	this.getName = function () {
		return this.name;
	};
	/* ... */
}

var instance = new ClassName('foo');
console.log(instance.__proto__);		// {constructor: ƒ}
console.log(ClassName.prototype);		// {constructor: ƒ}

上面的代码中ClassName就是构造函数(constructor),使用new关键字触发。这便是Constructor Invocation。

注意对于constructor,一定要使用new关键字触发。

这是因为new关键字一定返回一个对象:

  • 对于不含return语句的构造函数:返回实例对象

  • 对于包含return语句的构造函数:

    • return的是对象,则返回这个指定的对象
    • return的是基础类型,则忽略reutrn语句,返回实例对象
  • 对于普通函数:返回空对象{}

  • 若不使用new命令,不含return语句的构造函数将返回undefined

那么当我们使用new命令时,具体发生了什么呢?

1️⃣创建一个空对象,作为将要返回的对象实例。
2️⃣将这个对象的原型(__proto__),指向构造函数的prototype属性
3️⃣将这个空对象赋值给函数内部的this关键字
4️⃣开始执行构造函数内部的代码。

可以看到

Constructor Invocation指的是类似于new ConstructorName(field)的调用方式。

Constructor Invocation里的this被绑定到了实例对象上

💡原理

JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系。

var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

这样的结构是很清晰的,问题在于属性的值可能是一个函数。

var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

var f = function () {};
var obj = { f: f };

// 单独执行 Function Invocation
f()

// obj 环境执行 Method Invocation
obj.f()

JavaScript 允许在函数体内部,引用当前环境的其他变量。

var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

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

上面代码中,函数体里面的this.x就是指当前运行环境的x。

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

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x;在obj环境执行,this.x指向obj.x

所以可以看出,JavaScript的this本质上是一个内存问题:

function触发时,function的地址是怎么获得的?

  • 通过对象的属性访问器访问(Method Invocation):this指向对象。
  • 直接通过函数地址访问(Function Invocation):this指向全局对象Window。
  • 构造函数:new运算符将this指向实例对象。

回想前面的例子:

var object = {
	methodName: function () {
		console.log(this);
	}
};
object.methodName();			// Method Invocation: {methodName: ƒ}			
(false || object).methodName();	// Method Invocation: {methodName: ƒ}
(object.methodName)();			// Method Invocation: {methodName: ƒ}

/* 下面的形式之所以为 Function Invocation
 * 是因为在使用属性访问器得到 function 的地址后,还有运算的过程
 * 运算完成后返回 function 的地址,然后使用 () 运算符触发
 * 这样的话就是直接通过函数地址触发函数了
 * 这时 this 指向全局对象 Window
 */
(object.methodName = object.methodName)();	// Window
(false || object.methodName)();				// Window
// 逗号运算符:对两个表达式求值并返回后一个表达式的值
(1, object.methodName)();					// Window

⚡注意事项

  • 避免多层this

    var object = {
    	methodName: function () {
    		console.log(this);
    		var func = function () {	// 触发时,直接使用函数地址
      			console.log(this);
    		}();	// Function Invocation
    	}
    };
    // 触发时,使用属性访问器获得函数地址
    object.methodName(); // Method Invocation
    -------------console----------------------
    >> {methodName: ƒ}
    >> Window {parent: Window, opener: null, top: Window, length: 0, frames: Window,}
    

    一个解决方法是在第二层改用一个指向外层this的变量。

    var object = {
    	methodName: function () {
    		console.log(this);
    		var that = this;
    		var func = function () {	// 触发时,直接使用函数地址
     			console.log(that);
    		}();	// Function Invocation
    	}
    };
    // 触发时,使用属性访问器获得函数地址
    object.methodName(); // Method Invocation
    -------------console----------------------
    >> {methodName: ƒ}
    >> {methodName: ƒ}
    

    事实上,使用一个变量固定this的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。

  • 避免数组处理方法里的this

    数组的mapforEach方法,允许提供一个函数作为参数。这个函数内部不应该使用this

    var object = {
    	msg: 'hello',
    	arr: [ 'a', 'b' ],
    	method: function func() {
     		this.arr.forEach(function (item) {
     			console.log(this);	// function 触发时,this被绑定到了全局对象Window上
      			console.log(this.msg + ' ' + item);
    		});
     	}
    };
    object.method();
    -------------console----------------------
    >> Window {parent: Window, opener: null, top: Window, length: 0, frames: Window,}
    >> undefined a
    >> Window {parent: Window, opener: null, top: Window, length: 0, frames: Window,}
    >> undefined b
    

    要解决这个问题可以将this作为第二个参数,传给forEach方法。也可以使用中间变量固定this

  • 避免回调函数里的this

    当回调函数调用时,往往运行环境(context)早已发生变化,所以尽量不要在回调函数里面使用this

JavaScript提供了三个方法,让我们灵活的指定运行环境,也就是this

🔗参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值