聊一聊Javascript的this原理
场景1:全局环境下的this
这种情况相对简单直接,函数在浏览器全局环境中被简单调用,非严格模式下this指向window;在 use strict指明严格模式下就是undefined;
function f1 ( ) {
console.log (this);
}
function f2 ( ) {
'use strict'
console.log (this);
}
f1(); // window
f2() // undefined
这样的题目比较基础,请再看:
const foo = {
bar: 10,
func: function ( ) {
console.log (this); // window
console.log (this.bar); // undefined
}
};
let fn1 = foo.func;
fn1();
这里的this仍然指向的window。虽然func函数在foo对象中作为方法被引用,但是再赋值给fn1之后,调用fn1的是window,执行仍然是在window的全局环境中。因此输出仍然是window和undefined。
如果调用改为:
const foo = {
bar: 10,
func: function ( ) {
console.log (this); // {bar: 10, func: ƒ}
console.log (this.bar); // 10
}
};
foo.func();
因为这个时候this指向的是最后调用它的对象,此时的foo.func()语句中this指向foo对象。
在执行函数时,如果执行函数中的this是被上一级的对象所调用(「foo.func()」),那么this指向的就是上一级的对象(这里是「foo」),否则指向全局对象「window」
场景2:上下文对象调用中的this
const person = {
name: 'cuiHua',
brother: {
name: 'Mike',
func: function ( ) {
return this.name
}
}
};
console.log (person.brother.func()); // Mike
在这种嵌套的关系中,「this」指向最后调用它的对象,因此输出的是 Mike
我们在看一道更复杂的题目,请跟我一起做好“应试”的准备:
const personA = {
name: 'cuiHuaA',
func: function ( ) {
return this.name
}
};
const personB = {
name: 'cuiHuaB',
func: function ( ) {
return personA.func()
}
};
const personC = {
name: 'cuiHuaC',
func: function ( ) {
let fn = personA.func;
return fn()
}
};
console.log (personA.func());
console.log (personB.func());
console.log (personC.func());
答案:cuiHuaA,cuiHuaA,undefined,你答对了吗?
- 第一个 console 最简单,this指向 「 person1 」,所以输出 cuiHuaA
- 第二个 console 的 personB.func(),最终调用的还是 personA.func(),所以this仍然指向「 person1 」,所以输出 cuiHuaA
- 第三个 let fn = personA.func 赋值之后,是 「裸奔」调用( fn() )相当于 window.fn(),因此这里的 this 指向 「 window 」,输出 undefined
如果我们需要输出 cuiHuaB ,该怎么做?
可能你会想到使用bind/call/apply来对 this 指向进行干预,如果不能使用bind/call/apply,有别的办法吗?
const personA = {
name: 'cuiHuaA',
func: function ( ) {
return this.name
}
};
const personB = {
name: 'cuiHuaB',
func: personA.func
};
console.log (personA.func());
console.log (personB.func());
还是应用那个重要的结论, this 指向最后调用它的对象, 在 func 执行时, 挂到 personB 对象上即可,我们提前进行了类赋值的操作。
场景3:bind/call/apply改变this指向
const foo = {
name: 'me',
func: function ( ) {
console.log (this.name);
}
};
const bar = {
name: 'you',
};
console.log (foo.func.call(bar));
将输出 you,这不难理解,但是对bind/call/apply的高级考察往往会结合构造函数以及组合式实现继承,实现继承的话题,我们会单独讲到。
场景4:构造函数和this
function Foo ( ) {
this.bar = 'cuiHua'
}
const foo = new Foo();
console.log (foo.bar);
这样的场景往往伴随着下一个问题 new 操作符调用构造函数,具体做了什么?
- 创建一个新对象
- 将构造函数的this指向这个新的对象
- 为这个对象添加属性、方法等
- 最终返回新对象
以上过程,也可以用代码表述
let obj = {};
obj.__proto__ = Foo.prototype;
Foo.call(obj);
当然这里对new的模拟是一个简单的基本版的。
需要指出的是,如果在构造函数中出现了显式 return 的情况,那么注意分为两种情况
function Foo( ) {
this.user = 'cuiHua'
const obj = {};
return obj
}
const foo = new Foo();
console.log(foo); // {}
console.log (foo.user);
将会输出 undefined,此时 foo 返回的是空对象 obj
function Foo( ) {
this.user = 'cuiHua'
return 1
}
const foo = new Foo();
console.log (foo); // Foo {user: "cuiHua"}
console.log (foo.user);
将会输出 cuiHua,也就是说 foo 是返回的目标实例对象 this。
结论:如果构造函数中显式返回一个值,且返回的是一个对象,那么this就指向这个返回的对象;如果返回的不是一个对象,那么this仍然指向实例。
场景5:箭头函数中的this指向
箭头函数使用this不适用以上标准规则,而是根据外层(嵌套或者全局)上下文作用域来决定。
const foo = {
func: function ( ) {
setTimeout(function ( ) {
console.log (this);
})
}
};
console.log (foo.func());
这段代码,this 出现在 setTimeout()中的匿名函数里,因此 this 指向 window对象。 如果需要 this 指向 foo 这个 object对象,可以巧用箭头函数解决:
const foo = {
func: function ( ) {
setTimeout( ( )=>{
console.log (this);
})
}
};
console.log (foo.func()); // {func: f}
单纯箭头函数中的 this 非常简单, 但是综合所以情况, 结合 this 的优先级考察。这个时候 this 指向并不好确定, 请仔细阅读。
场景6: this优先级相关
我们常常把通过 call、appliy、bind、new 对 this 绑定的情况称为显式绑定; 根据调用关系确定的 this 指向称为隐式绑定。
function foo (a) {
console.log (this.a);
}
const obj1 = {
a: 1,
foo: foo
};
const obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2);
obj2.foo.call(obj1);
输出分别为2,1,也就是说call、apply的显式绑定一般来说优先级比隐式绑定更高。
function foo (a) {
this.a = a;
}
const obj1 = {};
let bar = foo.bind(obj1);
bar(2);
console.log (obj1.a);
上述代码通过 bind,将bar函数中的 this 指向 obj1 对象。现在的this是obj1, 执行 bar(2) 后, 「 this.a = a 」即 obj1.a = 2;即经过 bar(2) 执行后,obj1 对象为:{a: 2}
当在使用 bar 作为构造函数时:
let baz = new bar(3);
console.log (baz.a);
将会输出3. bar 函数本身是通过 bind 方法构造的函数, 其内部已经将 this 绑定为 obj1, 它再作为构造函数, 通过 new 调用时,返回的实例已经与 obj1 解绑。
new 绑定修改了 bind 中的this, 因此 new 的优先级比显示 bind 绑定更高。
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));
结果会输出2, 由于 foo() 的 this 绑定到 obj1, bar (引用箭头函数) 的 this 也会绑定 obj1, 箭头函数的绑定无法被修改。
如果将 foo 完全写成箭头函数的形式:
var a = 123;
const foo = () => a => {
console.log (this.a);
};
const obj1 = {
a: 2
};
const obj2 = {
a: 3
};
let bar = foo.call(obj1);
console.log (bar.call(obj2));
将会输出123;
这里我用了 var 声明 a = 123;我们看一下仅仅将上述代码变量 a 的赋值改为:
const a = 123;
const foo = () => a => {
console.log (this.a);
};
const obj1 = {
a: 2
};
const obj2 = {
a: 3
};
let bar = foo.call(obj1);
console.log (bar.call(obj2));
答案将会输出 undefined,原因是因为使用 const 声明的变量不会挂载到 window 全局对象当中。 因此 this 指向 window 时, 自然也就找不到 a 变量。