在理解this关键字之前,我们应该先搞清楚一个问题。
为什么要用this
来看看这段代码:
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context)
console.log(greeting);
}
var me = {
name: "pw"
}
var you = {
name: "Reader"
}
speak(you);
speak(me);
注:toUpperCase()
是一个字符串(String)对象的方法。它用于将调用它的字符串值转换为全部大写,并返回新的全部为大写的字符串。原字符串不会被改变,因为字符串在 JavaScript 中是不可变的。
执行结果:
Hello, I'm READER
Hello, I'm PW
再来看看使用了this
关键字的版本:
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call(this)
console.log(greeting);
}
var me = {
name: "pw"
}
var you = {
name: "Reader"
}
speak.call(you);
speak.call(me);
执行结果同上。
不难看出,如果不使用this
,你就必须要显式地传入上下文对象,而this
提供了方式让对象引用可以被隐式传递,优雅、简洁、且易于复用。上述代码逻辑简单,你可能还体会不出来,但是随着代码的设计模式越来越复杂,显式传递上下文对象会让代码变的越来越混乱,而this
可以让函数自动引用合适的上下文对象,使用this
关键字对API的设计是非常有帮助的。
另外,this
由于其不同的绑定方式(后面会提到),可以帮我们确定函数是如何被调用的(函数被谁调用以及被调用的位置)。
现在,让我们回到对this
关键字概念上的理解。
什么是this
this
是函数运行环境的指针,它指向当前执行上下文的对象。
this
的值是在运行时基于函数的调用方式动态绑定的,而不是定义时确定的。
不必咬文嚼字,理解了this的底层机制你就能看懂这两句话了。
this的机制
如果你不能轻松判断this的指向,我想告诉你的是:当你深入了解JavaScript中内存管理(包括栈内存和堆内存)的机制时,你就能更好地理解this
关键字在不同情况下指向哪里。
现在我需要你从内存的角度思考。 来看看这个例子:
var obj = {
foo:5
}//这是一个对象
-
当你定义一个对象并为其添加属性时,JavaScript引擎会在内存中为该对象创建相应的内部特性(即属性描述符):
-
[[value]]
:属性的值。 -
[[writable]]
:是否可以修改属性的值。 -
[[enumerable]]
:是否可以在遍历对象时被枚举。 -
[[configurable]]
:是否可以删除该属性或修改其描述符(是否可配置)。
-
var obj = {
foo:function(){
console.log(this);
}
}//这是一个对象
obj.foo();// 属于对象的方法被调用
var foo = obj.foo;
foo();// 属于普通函数被调用
反常识的地方来了,尽管foo
是对象obj
的属性并且函数function
是属性foo
的值,但是在内存中obj
与function
的联系并不是很强烈,因为foo
属性的值实际上是function
的地址。
再来看看两种不同的调用方式。相同点是它们最终都是同一个函数在运行;区别在于它们调用的方式不一样。
执行结果:
{ foo: [Function: foo] }
<ref *1> Object [global] {
省略
}
同一个函数的执行为什么会产生两种不同的结果?
这是因为 每个函数都有一个内置的this
关键字 ,foo属性作为对象的方法被调用在内存中指向了它的对象obj,
而foo作为普通函数调用则默认指向了全局对象,现在我们需要了解this的指向的规则了。
this的四种绑定方式
1. 默认绑定
这是最基础的绑定规则,适用于直接调用函数的情况。
- 非严格模式:如果函数不是作为对象的方法被调用、也不是通过
new
或显式绑定的方式调用,那么this
会默认指向全局对象(在浏览器中是window
,在Node.js环境中是global
)。 - 严格模式:在严格模式下,如果没有任何其他规则适用,
this
将被设置为undefined
。
function foo() {
console.log(this);
}
foo(); // 在非严格模式下输出 window 或 global,在严格模式下输出 undefined
2. 隐式绑定
当一个函数作为对象的方法被调用时,this
会被隐式地绑定到该对象上。换句话说,如果一个函数是某个对象的一个属性,并且通过该对象来调用这个函数,那么this
就会指向这个对象。
function foo(){
console.log(this.a);
}
var obj1={
a:2,
foo:foo
};
obj.foo();
需要注意的是,如果方法被赋值给一个变量后调用,或者是在某些情况下传递给另一个函数时调用,隐式绑定可能会丢失,这时this
会根据调用位置应用默认绑定规则。
3. 显式绑定
你可以使用call()
、apply()
或bind()
方法来显式地指定函数内部this
的值。这种方式允许你手动控制this
的绑定。
call()
和apply()
:立即调用函数,并允许你指定this
的值。它们的区别在于参数的传递方式不同:call()
接收的是逗号分隔的参数列表,而apply()
接收的是一个参数数组。bind()
:返回一个新的函数,该函数的this
被永久绑定到你提供的对象上,即使之后再使用call()
、apply()
或再次bind()
也不会改变其this
的绑定。
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const person = { name: 'Alice' };
greet.call(person, 'Hello'); // Hello, Alice
greet.apply(person, ['Hi']); // Hi, Alice
const greetPerson = greet.bind(person);
greetPerson('Good morning'); // Good morning, Alice
4. new绑定(构造器绑定)
当使用new
关键字调用一个函数时,它被称为构造函数调用。在这种情况下,this
会被绑定到新创建的对象实例上。构造函数通常用于创建具有特定属性和方法的新对象。
function Constructor(name) {
this.name = name;
this.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
}
const instance = new Constructor('Bob');
instance.sayHello(); // Hello, Bob
构造器调用还涉及到原型链的概念,新创建的对象会继承构造函数的原型上的属性和方法。
现在明白为什么在创建构造函数的时候我们总是要写this
在里面了吗?在代码的世界里,绝不存在什么“固定搭配”,每个关键字、每种代码规范都有它的设计初衷,只有深入到底层,你才能明白这些设计的真正用法。
绑定可能有问题
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;
var a = "oops, global";
bar();
输出结果:
oops, global
仔细观察这段代码和隐式绑定的区别,我们可以看到代码中定义了bar
变量并将对象obj下的属性foo赋值给了它。
然后在对象外部定义了一个变量a
,此时对象内外都存在一个变量a
。
那么,为什么this
选择了外部的a
而没有选择内部的a
呢?
还记得this的定义么?我来重复一遍吧。
this
是函数运行环境的指针,它指向当前执行上下文的对象。
this
的值是在运行时基于函数的调用方式动态绑定的,而不是定义时确定的。
请看到代码的最后一行,foo
函数被赋给了变量bar
,由于JavaScript是弱类型语言,此时bar
同样为函数,而bar
实际上是以普通函数的方式调用的,因此this
默认指向全局,所以拿到了全局执行上下文中的a
(严格模式下将被设置为undefined
)。像这样隐式绑定的函数丢失其绑定对象的情况我们称之为隐式丢失。
this丢失的解决方法
下面这段代码会报错: TypeError: this.func1 is not a function
<script>
var name = "windowName";
var a = {
name:"肖宇泉",
func1:function() {
console.log(this.name);
},
func2:function() {
setTimeout((function() {
this.func1();
}),1000)
}
}
a.func2();
</script>
解决方法有两种:
1.保存this
引用
<script>
var name = "windowName";
var a = {
name:"肖宇泉",
func1:function() {
console.log(this.name);
},
func2:function() {
let _this = this;
setTimeout((function() {
_this.func1();
}),1000)
}
}
a.func2();
</script>
- 将当前的
this
值保存到_this
变量中。通过保存对原始this
的引用,可以在回调中正确引用对象a
的属性和方法。
2.使用箭头函数
<script>
var name = "windowName";
var a = {
name:"肖宇泉",
func1:function() {
console.log(this.name);
},
func2:function() {
setTimeout(( ()=> {
this.func1();
}),1000)
}
}
a.func2();
</script>
- 箭头函数是ES6(ECMAScript 2015)引入的一种新的函数定义方式,它简化了函数的语法,并且改变了
this
的绑定规则。它不拥有自己的this
,它继承自外围(词法)作用域,即定义箭头函数的地方。这意味着在箭头函数内部,this
的值是在函数被创建时确定的,而不是在函数被调用时确定的。
在一般情况下this
的指向应该是在函数被调用时确定的,而上述两种方式使this
的指向在函数被创建时就确定了,因此两种方法本质上是相同的。
希望这篇文章对你有帮助,点个赞再走吧~