this介绍
在 JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象下运行,而 this 就是函数运行时的环境。
JavaScript 支持运行环境动态切换,也就是说 this 的指向是动态的,在函数定义的时候是确定不了的,只有函数执行的时候才能确定,实际上 this 的最终指向的是那个调用它的对象。
this 动态性背后的原理
在 JavaScript 中,变量的实质是一个指向原始对象的内存地址,原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象,其中[[value]]
对应属性的值。
当属性的值是一个函数,此时函数又单独保存在内存里,属性的值又是该函数的地址。
由于函数是独立于原始对象单独保存的,因此可以在不同的环境中执行,而 this 的设计就是为了方便在函数体内部直接获得当前的运行环境。
不同场景下 this 指向
全局环境
在浏览器全局环境下,this 始终指向全局对象(window), 无论是否严格模式;
普通函数,非严格模式下,指向 window。严格模式下,指向 undefined。
构造函数
构造函数中的 this,指的是实例对象。
window.identity = "The Window"
function Obj() {
this.identity = "My Object"
this.getIdentityFunc = function () {
console.log(this.identity)
}
this.getIdentityFunc1 = () => {
console.log(this.identity)
}
}
const obj = new Obj() // new 关键字可以改变 this 的指向,将 this 指向对象 obj
obj.getIdentityFunc() // My Object
obj.getIdentityFunc1() // My Object
补充:new 关键字会创建一个空的对象,然后会自动调用一个函数 apply 方法,将 this 指向这个空对象,这样的话函数内部的 this 就会被这个空的对象替代。
对象方法
对象没有自己的上下文,对象的方法里面包含 this,this 指向就是方法运行时所在的对象。
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
// 注意1:下面运行环境已经变成全局环境
var bar = obj.foo; bar() // window
(obj.foo = obj.foo)() // window
(false || obj.foo)() // window
(1, obj.foo)() // window
// 注意2: 对象嵌套不会向上找
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined,因为实际调用 m 的是 b,b 中不存在 p 这个属性
数组方法
数组的方法,如map
和foreach
,允许提供一个函数作为参数。这个函数内部不应该使用this。
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) { // 执行 o.f() 时,this 指向 o
console.log(this.v + ' ' + item); // this 是 window,取不到 p 的值
});
}
}
// 修改1: 使用中间变量固定
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
var that = this;
this.p.forEach(function (item) {
console.log(that.v+' '+item);
});
}
}
// 修改2: 利用第二个参数,固定运行环境
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(that.v+' '+item);
}, this);
}
}
// 修改3: 使用箭头函数
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach((item) => {
console.log(that.v+' '+item);
});
}
}
原型链
原型链中的方法的 this 仍然指向调用它的对象。
var o = {
f : function(){
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
箭头函数
箭头函数没有自己的 this,箭头函数的 this 就是上下文中定义的 this。
var identity = "window"
var obj = {
identity: 'object',
testIdentity: this.identity, // 指向对象所在上下文
getIdentityFunc: () => {
console.log(this.identity)
}
}
console.log(obj.testIdentity) // window
obj.getIdentityFunc() // window
- 箭头函数不能用做构造函数,因为没有它自己的this
- call / apply / bind 方法对于箭头函数来说只是传入参数,无法改变 this 的指向
DOM 事件处理函数
事件处理函数内部的 this 指向触发这个事件的 DOM 元素对象。
定时器
对于定时器内部的回调函数的 this 指向全局对象 window。
注意:定时器、数组方法等,通过回调的方式传入函数,函数中如果有 this,this 的指向很可能会出错。一般可以通过 bind 方法,固定执行环境。有些直接支持 this 作为参数传入。
如何改变 this 指向
JavaScript 提供了 call、apply、bind 这三个方法,来切换/固定 this 的指向。
func.call(thisValue, arg1, arg2, ...)
func.apply(thisValue, [arg1, arg2])
func.bind(thisValue, arg1, arg2)()
- call 的参数一个个添加,可用来调用对象的原生方法
- apply 接收一个数组作为函数执行时的参数,方便调用数组的原生方法
- bind 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数,其中arg1, arg2... 是当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
如何实现 call
将函数的引用 this 赋值给 call 的第一个参数 obj,然后通过 obj 引用调用函数。
Function.prototype.myCall = function() {
let obj = arguments[0] || window; // 如果没有传this参数,this将指向window
let args = [...arguments].slice(1); // 获取第二个及后面的所有参数(arg是一个数组)
let fn = Symbol(); // Symbol属性来确定fn唯一
obj[fn] = this; // this指向调用myCall的函数(代指a函数),同时将a函数的引用赋值给obj的fn属性。此时,当a函数调用的时候就是指向obj的了
let res = obj[fn](...args); // a函数的引用调用,指向obj,也就是传入的对象
delete obj[fn]; // 不能增加obj的属性,所以要删除
return res; // 如果a函数有返回值这里就有返回值,如果a函数没有返回值,同样的这里也没有
}
如何实现 apply
类似 call,区别在于参数格式。
Function.prototype.myApply = function() {
let obj = arguments[0] || window; // 如果没有传this参数,this将指向window
let args = arguments[1]; // 获取参数(arg是一个数组)
let fn = Symbol(); // Symbol属性来确定fn唯一
obj[fn] = this; // this指向调用的函数
let res = obj[fn](...args); // a函数的引用调用,指向obj,也就是传入的对象
delete obj[fn]; // 不能增加obj的属性,所以要删除
return res; // 返回a函数的返回值
}
如何实现 bind
同 call 和 applay 不同的是,bind返回一个函数,不是立即调用。
Function.prototype.myBind = function (object) {
let obj = object || window; // 如果没有传this参数,this将指向window
let fn = this; // this指向调用的函数
let arg = [...arguments].slice(1); // 获取第二个及后面的所有参数(arg是一个数组)
const fBind = function () {
/* 如果当前函数执行中的this是fBind的实例,说明是fBind被new了,那么当前this就是函数的实例,否则是obj */
let o = this instanceof fBind ? this : obj;
fn.applay(o, args.concat(...arguments));
}
// 考虑实例化后对原型链的影响
let temp = function() {};
temp.prototype = fn.prototype;
fBind.prototype = new temp();
return fBind; // 返回一个函数
}