JS 详谈This指向
从原理出发
什么是this
? this
是被调用函数对调用它上下文对象的引用。
首先我们围绕耳熟能详的“this
始终指向它的调用者”开始。这句结论虽然没有什么问题,但是说得过于笼统,还是得深入到背后的执行原理才能举一反三解决问题。
举个简单的例子:
var obj = {
num: 2;
foo: function () { console.log(this.num) }
};
var foo = obj.foo;
var num = 3;
obj.foo();//2
foo();//3
①obj.foo();
是对象obj
调用自身的属性(方法)foo
,this
指向调用者obj
。
②由var foo = obj.foo;
获取函数,并用foo();
在全局作用域中调用函数,所以this
指向浏览器全局对象Window
。
内存数据结构的概述
JS之所以设计this
,是因为其内存的数据结构的特征。
var obj = { foo: 5 };
①上面的对象赋值给变量,其实质是JS引擎先在内存中生成{foo: 5}
,然后再把该对象的内存地址赋值给变量obj
。也就是说onj.foo
实质上是先从obj
获取对象的内存地址然后再从地址读取原始对象,最后返回对象属性foo
的值。
②原始对象以词典结构保存,一个属性(数据属性和存取属性: get&set)对应一个属性描述符对象。如下图,foo
属性的描述对象就包含4个描述属性,而最重要的值保存在描述对象[[value]]
中。
当foo
属性值为函数时,JS引擎同样会先将原始函数保存在内存中,然后把该函数的内存地址存放于foo
属性的描述对象中的[[value]]
里面。
属性描述符对象共有的可选键值:
可选键 | 键值 | 说明 | 数据属性 | 存取属性 |
---|---|---|---|---|
configurable | 布尔值,默认为 false | 所有可选键是否可配置以及是否能删除该属性 | ✔ | ✔ |
enumerable | 布尔值,默认为 false | 是否可枚举 | ✔ | ✔ |
value | JS有效值,默认为 undefined | 该属性的值 | ✔ | |
writable | 布尔值,默认为 false | 是否可写 | ✔ | |
get | getter函数,默认值 undefined | 访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象。该函数的返回值会被用作属性的值 | ✔ | |
set | setter函数,默认值 undefined | 当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象 | ✔ |
从原理出发章节参考于JavaScript 的 this 原理——阮一峰的网络日志
为何设计this
当我们我们需要 函数A 执行在 上下文环境B 中时就需要利用 this 为 A 指定执行环境 B,而不是又在 环境B 中再声明一边 函数A。就像这样:
var f = function (){
console.log(this.x)
};
var x = 1;//全局变量 x
var obj = {
x: 2,
f: f
}
f();//结果为1,执行于全局环境,所以 this.x 指向 Window.x
obj.f();//结果为2,执行于 obj 内部环境,所以 this.x 指向 obj.x
由此可见,this
是一个指针型变量,它指向当前函数的运行环境。
不同使用/调用情况下的this
在一个对象的方法中,
this
指向该对象。
直接单独使用,this
指向全局对象。
在一个函数中,this
指向全局对象(strict mode
指向undefined
)。
在JS事件中,this
指向接受了该事件的元素对象
。
最后使用方法call()
,apply()
,bind()
可以将this
指向任何对象。
可能你会有疑问,为什么不直接说指向全局对象window
,而仅说明指向全局对象。
这是因为在JS中全局对象也分三种情况:
- 在浏览器中,JS没有专门用作后台任务启动的任何代码都将
Window
作为其全局对象。web应用绝大多数代码如此。 - 在
Worker
中运行的代码将WorkerGlobalScope
对象作为其全局对象。 - 在
Node.js
环境下运行的脚本具有一个称为global
的对象作为其全局对象。
注意:this
不是一个JS变量而是指针型变量,它仅是访问函数执行环境的关键字,您不能更改它的值
this
的扩展延伸
new
关键字与this
关键字
例1:
function name () {
this.fne = "yulin"
};
var a = new name();
console.log(a.fne);//yulin
构造函数中的 this 指向:
①:首先new
关键字会创建一个用户定义或者具有构造函数实例的内置对象。在上面的例子new
关键字创建一个空对象name {}
。
②:然后构造函数name()
的this
指向对象name {}
的内存地址,再执行name()
并返回执行结果,所以空对象成为实例对象name {fine: 'yulin'}
。
③:最后整个对象name {fne: 'yulin'}
赋值给变量a
,变量a
引用对实例象name
,实质上是变量a
取得了实例对象name
的内存地址。
例2:MDN官方示例解析:
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);// 给 product() 指定一个调用 Food() 的 this 对象。
this.category = 'food';
}
console.log(new Food('cheese', 5).name);// 构造一个 food() 实例,food() 内的 this 指向 food() 实例对象,所以 product() 变更 this 指向 food() 实例
// expected output: "cheese"
this
与return
的问题
上文说到构造函数执行的返回结果的有几种情况:
- 构造函数返回对象时,
this
会引用返回的对象,null
除外,否则指向起始调用者。
function name (){
this.fne = "yulin";
return {}
};
var a = new name();
console.log(a.fne);//undefined,指向 {}
function name (){
this.fne = "yulin";
return function () {}
};
var a = new name();
console.log(a.fne);//undefined,指向 function () {}
function name (){
this.fne = "yulin";
return null
};
var a = new name();
console.log(a.fne);//yulin,指向 name {fne: 'yulin'},null是特殊的对象不会更改 this
function name () {
this.fne = "yulin";
return undefined
};
var a = new name();
console.log(a.fne);//yulin,指向 name {fne: 'yulin'}
指定this
的值
-
当我们需要在指定的环境中执行一个函数时,我们就需要用到
call()
方法指定this
的值,第一个参数为this
的值,第二个参数是传入该函数的分隔参数。const a = { name: 'yulin', fn: function (e, q) { console.log(this.name); console.log(e + q); } }; const b = { name: 'yhh' } var x = a.fn x.call(b, 2, 3);//yhh 5
-
apply()
方法与call()
类似,但传入apply
的第二个参数必须是数组或类数组对象(数组、集合、参数对象等等),就像[1, 2, 3]
,[a , b, c]
以及数组变量。2.1 参数上的特性给数组对象带去了极大的便利:
// 获取数组中最大值 Math.max.apply(null, [1, 2, 3])// 3 Math.max.apply(Math, [1, 2, 3])// 3 Math.max.apply('', [1, 2, 3])// 3 Math.max.apply(0 , [1, 2, 3])// 3 // 用 apply 将数组各项添加到另一个数组 const array = ['a', 'b']; const elements = [0, 1, 2]; array.push.apply(array, elements);
注意:如果按上面方式调用 apply,有超出 JavaScript 引擎参数长度上限的风险。一个方法传入过多参数(比如一万个)时的后果在不同 JavaScript 引擎中表现不同。(JavaScriptCore 引擎中有被硬编码的参数个数上限:65536)。
2.2 如果你的参数数组可能非常大,那么推荐使用下面这种混合策略:将数组切块后循环传入目标方法
function minOfArray(arr) {
let min = Infinity;
let QUANTUM = 32768; //最大切割长度,刚好是参数数量限定的最大值一半
//数组长度小于32768只切割一次,大于32768且小于65536切割两次,大于65536小于(65536+32768)三次,以此类推
for (let i = 0, len = arr.length; i < len; i += QUANTUM) {
const submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
min = Math.min(submin, min);
}
return min;
}
let min = minOfArray([5, 6, 2, 3, 7]);
2.3 使用apply
来链接构造器
下面例子中我创建一个全局Function
对象的construct
方法 ,让构造器能够使用一个类数组对象参数而非参数列表。
// 给全局函数对象添加 construct 方法
Function.prototype.construct = function (aArgs) {
const oNew = Object.create(this.prototype); // 目的是得到原型为 MyConstructor 的对象
this.apply(oNew, aArgs);
/* “原汤化原食”
通过 apply 链接 this 指向的构造器函数,
调用者是该构造器函数创建的对象实例
*/
return oNew;
};
function MyConstructor() { // 传入不确定数量的参数,使用arguments无需arguments作为形参
for (let nProp = 0; nProp < arguments.length; nProp++) {
this['property' + nProp] = arguments[nProp];// this 指向的调用者对象更新属性
}
}
let myArray = [4, 'Hello world!', false];
let myInstance = MyConstructor.construct(myArray);
console.log(myInstance.property1); // logs 'Hello world!'
console.log(myInstance instanceof MyConstructor); // logs 'true'
console.log(myInstance.constructor); // logs 'MyConstructor'
同理,我将construct
方法稍加修改也能得到我预期的结果。
Function.prototype.construct = function (arr) {
const oNew = Object.create({});// 以字面量对象 Object 为原型创建“白板”对象
this.apply(oNew, arr);
return oNew;
}
function MyConstructor() {
let len = arguments.length
for (let i=0; i<len; i++) {
this['property' + i] = arguments[i];
}
}
let myArray = [4, 'Hello world!', false];
let myInstance = MyConstructor.construct(myArray);
console.log(myInstance.property1); // logs 'Hello world!'
console.log(myInstance instanceof MyConstructor); // logs 'false'
console.log(myInstance.constructor); // logs 'Object'
bind()
创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
3.1 普通使用情形
3.2const a = { name: 'yulin', fn: function (e, q) { console.log(this.name); console.log(e + q); } }; const b = { name: 'yhh' } var x = a.fn x.bind(b); /*ƒ (e, q) { console.log(this.name); console.log(e + q); }*/ let y = x.bind(b); y(2, 3);//yhh 5
bind
解决回调函数丢失this
一个函数不论原先它的this
指向谁。一旦作为回调函数(或定时回调)执行,它将遗忘原有的this
并指向全局对象(严格模式同样适用)
例子1:
例子2:const person = { firstName:"John", lastName: "Doe", display: function () { console.log(this.firstName + " " + this.lastName); } } person.display();// 'John Doe' setTimeout(person.display, 3000);// undefined undefined
对于上面var firstName = 'yu', lastName = 'lin'; const person = { firstName:"John", lastName: "Doe", display: function () { console.log(this.firstName + " " + this.lastName); } } person.display(); // 'John Doe' setTimeout(person.display, 3000); // 'yu lin'
this
的丢失,有两种方法可以解决:
推荐使用箭头函数作为回调函数,箭头函数不会创建自己的this
,它只会从自己的作用域链的父执行上下文继承this
。并且作为回调函数不会丢失this
值:
使用函数var firstName = 'yu', lastName = 'lin'; const person = { firstName:"John", lastName: "Doe", display: () => { console.log(this.firstName + " " + this.lastName); } } let display = person.display.bind(person);// 返回 this 指向 person 的绑定函数 display setTimeout(display, 3000); // 'yu lin'
bind
方法绑定this
:var firstName = 'yu', lastName = 'lin'; const person = { firstName:"John", lastName: "Doe", display: function () { console.log(this.firstName + " " + this.lastName); } } let display = person.display.bind(person);// 返回 this 指向 person 的绑定函数 display setTimeout(display, 3000); // 'John Doe'