在 Java 等面向对象的语言中,this 关键字的含义是明确且具体的,即指代当前对象。一般在编译期绑定。而 在 JavaScript 中,this 是动态绑定,或称为运行期绑定的 ,由于其运行期绑定的特性,JavaScript 中的 this 含义要丰富得多,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。
Q1:为什么要用this
呢?
function identify(){
return this.name.toUpperCase();
}
function speak(){
var greeting = "Hello,I'm " + identify.call(this);
console.log(greeting);
}
var me = { name: 'BubbleM', speak: speak };
me.speak(); // Hello,I'm BUBBLEM
上面这段代码可以在不同的上下文对象(me和you)中重复使用函数identify和speak,不用针对每个对象编写不同版本的函数。
如果不使用this,那就需要给identify和speak显式传入一个上下文对象:
function identify(context){
return context.name.toUpperCase();
}
function speak(context){
var greeting = "Hello,I'm " + identify(context);
console.log(greeting);
}
var me = { name: 'BubbleM', speak: speak };
me.speak(me); // Hello,I'm BUBBLEM
A1:this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并易于复用。
this指向谁?
多数情况下,this 指向调用它所在方法的那个对象。当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined)
⚠️ this 的指向是在调用时决定的,而不是在书写时决定的。这点和闭包恰恰相反。
// 声明位置
var me = {
name: 'xiuyan',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}
var you = {
name: 'xiaoming',
hello: function() {
var targetFunc = me.hello
targetFunc()
}
}
var name = 'BigBear'
// 调用位置
you.hello()
调用位置输出的结果是 BigBear—— 竟然不是 xiaoming?的确,我们打眼看过去,直觉上肯定会认为是 you 这个对象在调用 hello 方法、进而调用 targetFunc,所以此时 this 肯定指向 you 对象啊!为啥会输出一个 window 上的 name 呢?
“this 指向调用它所在方法的那个对象”
回头看 targetFunc 这个方法,之所以第一直觉会认为它的 this 应该指向 you 这个对象,其实还是因为把 “声明位置” 和 “调用位置” 混淆了。我们看到虽然 targetFunc 是在 you 对象的 hello 方法里声明的,但是在调用它的时候,我们是不是没有给 targetFunc 指明任何一个对象作为它前缀? 所以 you 对象的 this 并不会神奇地自动传入 targetFunc 里,js 引擎仍然会认为 targetFunc 是一个挂载在 window 上的方法,进而把 this 指向 window 对象。
一定要记住“不管方法被书写在哪个位置,它的 this 只会跟着它的调用方走”这个核心原则。
特殊情境下的this指向
在三种特殊情境下,this 会 100% 指向 window:
- 立即执行函数(IIFE),即定义后立刻调用的匿名函数;
var name = 'BigBear'
var me = {
name: 'xiuyan',
// 声明位置
sayHello: function() {
console.log(`你好,我是${this.name}`)
},
hello: function() {
(function(cb) {
// 调用位置
cb()
})(this.sayHello)
}
}
me.hello() // BigBear
立即执行函数作为一个匿名函数,在被调用的时候,我们往往就是直接调用,而不会(也无法)通过属性访问器( 即 xx.xxx) 这样的形式来给它指定一个所在对象,所以它的 this 是非常确定的,就是默认的全局对象 window。
- setTimeout 中传入的函数;
- setInterval 中传入的函数;
var name = 'BigBear'
var me = {
name: 'xiuyan',
hello: function() {
setTimeout(function() {
console.log(`你好,我是${this.name}`)
})
}
}
me.hello() // 你好,我是BigBear
我们所看到的延时效果(setTimeout)和定时效果(setInterval),都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交付到全局对象手上。因此,函数中 this 的值,会被自动指向 window。
严格模式下的this指向
- 普通函数中的 this 在严格模式下的表现:
所谓 “普通函数” ,这里我们是相对于箭头函数来说的。在非严格模式下,直接调用普通函数时,函数中的 this 默认指向全局变量(window 或 global)
function showThis() {
console.log(this)
}
showThis() // 输出 Window 对象
而 在严格模式下,this 将保持它被指定的那个对象的值,所以,如果没有指定对象,this 就是 undefined :
'use strict'
function showThis() {
console.log(this)
}
showThis() // undefined
- 全局代码中的 this 在严格模式下的表现:
'use strict'
console.log(this) // 直接在全局代码里尝试去拿 this Window
'use strict'
var name = 'BigBear'
var me = {
name: 'xiuyan',
hello: function() {
// 全局作用域下实现的延时函数
setTimeout(function() {
console.log(`你好,我是${this.name}`)
})
}
}
me.hello() // 你好,我是BigBear
像这样 处于全局代码中的 this, 不管它是否处于严格模式下,它的 this 都指向 Window(这点要特别注意,容易误以为这里也是 undefined )。
构造函数中的this
当我们使用构造函数去 new 一个实例的时候:
function Person(name) {
this.name = name
console.log(this)
}
var person = new Person('xiuyan')
构造函数里面的 this 会绑定到我们 new 出来的这个对象person
上。
Q2:使用new来调用函数,或者发生构造函数调用时发生了什么?
- 创建一个全新的对象
- 这个新对象会执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
箭头函数中this的指向
箭头函数和闭包很相似,都是认“死理”—— 认“词法作用域”的家伙。所以说 箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定(和普通函数的 this 规则恰恰相反~)
var name = 'BigBear'
var mefun = function(){
this.name = 'xiuyan';
return () => {
console.log(this.name)
};
}
var me = {
name: 'xiuyan',
// 声明位置 此时作用域是全局作用域
hello: () => {
console.log(this.name)
}
}
// 调用位置
me.hello() // BigBear
mefun()(); // xiuyan
如何改变this的指向?
- 通过改变书写代码的方式做到(比如箭头函数)
当我们将普通函数改写为箭头函数时,箭头函数的 this 会在书写阶段(即声明位置)就绑定到它父作用域的 this 上。无论后续我们如何调用它,都无法再为它指定目标对象 —— 因为 箭头函数的 this 指向是静态的,“一次便是一生”。 - 显式地调用一些方法来帮忙,如
call
、apply
、bind
- call、apply 和 bind 之间的区别比较大:
call
、apply
在改变 this 指向的同时,直接执行目标函数;
bind
则只负责改造 this,不作任何执行操作,返回一个绑定上下文的函数。 - call 和 apply 之间的区别,则体现在对入参的要求上:
call
只需要将目标函数的入参逐个传入即可;
apply
则希望入参以数组形式被传入。
模拟实现call
方法 实现核心思想
- 改变 this 的指向,将 this 绑到第一个入参指定的的对象上去;
- 根据输入的参数,执行函数。
Function.prototype.myCall = function(context, ...args){
context.func = this;
context.func(...args);
delete context.func;
}
Function.prototype.myApply = function(context, arr){
context.func = this;
context.func(...arr);
delete context.func;
}
Function.prototype.myBind = function(context, ...args){
let self = this;
return function(){
self.call(context, ...args)
}
}
var name = 'global';
var me = {
name: 'hah'
}
function showName(){
console.log(this.name);
}
function showFullName(firstNama, lastName){
console.log(`${this.name} ${firstNama}_${lastName}`);
}
showName(); // global
showName.myCall(me); // hah
showFullName.myCall(me, 'Bubble', 'M'); // hah Bubble_M
showFullName.apply(me, ['Bubble', 'M']); // hah Bubble_M
showFullName.myApply(me, ['Bubble','M']); // hah Bubble_M
showFullName.bind(me, 'Bubble', 'M')(); // hah Bubble_M
showFullName.myBind(me, 'Bubble', 'M')(); // hah Bubble_M
扩充思考:
Q:如果我们第一个参数传了 null 怎么办?是不是可以默认给它指到 window 去?A:context = context || global;
Q:函数如果是有返回值的话怎么办?是不是新开一个 result 变量存储一下这个值,最后 return 出来就可以了?
Function.prototype.myCall = function(context, ...args){
context = context || window;
context.func = this;
let result = context.func(...args);
delete context.func;
return result;
}