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 5bind解决回调函数丢失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'

浙公网安备 33010602011771号