【JavaScript】this关键字的指向问题(五千字详解)

前言

看了这篇文章,麻麻再也不用担心我不理解JS中this的指向问题了。本文以文章目录为顺序,层层递进。耐心的看完,会有很大的收获。

一、this的作用?我们为什么要用this,没它不行吗?

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的JavaScript开发者也很难说清它到底指向什么。

实际上,JavaScript中this的机制并没有那么先进,但是开发者往往会把理解过程复杂化。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
this都有一个共同点,它总是返回一个对象。简单说,this就是属性或方法“当前”所在的对象。

从某些角度来说,开发中如果没有this,很多问题也有其他的解决办法。
但是没有this,会让我们编写代码变得非常不放便。

话不多说,举个栗子 看看就知道了,示例代码如下:

var obj = {
    
    
    name: '张三',
    eating () {
    
    
        console.log(obj.name + '在吃饭')
    },
    studying () {
    
    
        console.log(obj.name + '在学习')
    },
    running () {
    
    
        console.log(obj.name + '在跑步')
    }
}

obj.eating()
obj.studying()
obj.running()
上述示例代码,如果我们修改了变量obj名字为object,之后需要修改的代码有以下两处:
首先,需要修改的代码有方法的调用。
其次,就是在eating、studying、running三个方法中打印语句中的变量名
如果我们的obj对象中不止三个方法呢?如果是一百个方法呢?一个一个改,杀了我吧(裂开)。

如下示例代码,展示了使用this进行优化的效果:

var obj = {
    
    
    name: '张三',
    eating () {
    
    
        console.log(this.name + '在吃饭')
    },
    studying () {
    
    
        console.log(this.name + '在学习')
    },
    running () {
    
    
        console.log(this.name + '在跑步')
    }
}

obj.eating()

优化后的代码不管我们怎么改变变量名称 ,我们需要修改的地方只有一个,那就是方法调用时的对象名称。是不是简便了许多,为编程的效率提升的不止一点哦。

二、this在全局作用域中指向哪里?

其实在大多数情况下,this都是出现在函数中。但也有出现在全局作用域下的情况,我们就来举个栗子说一说。

示例代码如下:

var name = 'code'
console.log(this) 
console.log(this.name) // 输出 code 
console.log(Window.name) // 输出 code 

浏览器环境下输出结果如下图所示:
在这里插入图片描述
由图可知,全局作用域下,浏览器环境中:this指向的是全局对象Window。

这里需要补充一下的是,在全局作用域下,node环境中,this指向的是一个空对象{}

三、同一个函数中this的不同指向

先看代码,再做结论。如下示例代码展示了不同的调用方法,得到的this的不同指向。示例代码如下:

// 定义一个函数
function foo () {
    
    
    console.log(this)
}

// 调用方式一:直接调用
foo() // this指向:Window

// 调用方式二:间接调用
var obj = {
    
    
    name: '张三',
    fn: foo
}

obj.fn() // this指向:obj对象


// 调用方式三:通过call/apply调用
foo.call('123') //this指向: String {‘123’}对象

浏览器输出结果如下所示:
在这里插入图片描述
三个不同的调用方法,得到了三个不同的this指向。

结论:
1.函数在调用时,JavaScript会默认给this绑定一个值;
2.this的绑定值与定义的位置没有关系;
3.this的绑定和调用方式以及调用的位置没有关系;
4.this是在运行时被绑定的,而不是在定义时。

四、this的四种绑定规则

1、默认绑定

在一个函数体中使用this,当该函数被独立调用时,就会被默认绑定一个对象。

在默认绑定情况下,只要是独立函数调用就是指向Window。

如下展示五个案例进行深入学习,层层深入。示例代码如下:

// 案例一:
function foo1 () {
    
    
    console.log(this)
}
foo1()


// 案例二: 三个方法被调用的时候都是独立的,所以this的指向都是Window
function one () {
    
    
    console.log(this)
}
function two () {
    
    
    console.log(this)
    one()
}
function three () {
    
    
    console.log(this)
    two()
}

three()


// 案例三:
var obj = {
    
    
    name: '张三',
    foo3 () {
    
    
        console.log(this)
    }
}

var bar3 = obj.foo3
bar3() // Window


// 案例四:
function foo4 () {
    
    
    console.log(this)
}
var obj = {
    
    
    name: '张三',
    fn4: foo4
}

var bar4 = obj.fn4
bar4() // Window


// 案例五:
function foo5 () {
    
    
    function bar () {
    
    
        console.log(this)
    }
    return bar
}
var fn = foo5()
fn()

上述代码中五个案例,全部都是独立调用,所以显而易见,输出结果this都是指向Window对象

2、隐式绑定

通过某个对象进行调用,也就是它的调用位置中,是通过某个对象发起的函数调用。
隐式绑定有一个前提条件

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性)
  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
  • 正式通过这个引用,间接的讲this绑定到了这个对象上

如下通过三个案例,进行隐式绑定的详细了解,示例代码如下:

// 案例一:通过隐式绑定将fn函数中的this指向obj对象
function fn () {
    
    
    console.log(this)
}
var obj = {
    
    
    name: '张三',
    studying: fn
}
obj.studying() // {name: '张三', studying: ƒ}


// 案例二:
var obj2 = {
    
    
    name: '张三',
    eating () {
    
    
        console.log(this.name + '正在吃饭')
    },
    running () {
    
    
        console.log(obj.name + '正在跑步')
    }
}
// 隐式绑定进行调用
obj2.eating()  // 张三正在吃饭
obj2.running() // 张三正在跑步

var fn = obj.eating
fn() // 独立函数调用 指向window,name为window中的name的值为空    输出结果为: 正在吃饭


// 案例三
var obj3 = {
    
    
    name: 'foo3',
    foo () {
    
    
        console.log(this)
    }
}
var obj4 = {
    
    
    name: 'foo4',
    bar: obj3.foo
}

obj4.bar() // {name: 'foo4', bar: ƒ}

上述三个案例中,三个this都被隐式绑定到了调用此方法的对象上。

3、显示绑定 (call/apply/bind

显示绑定就是指使用js中的原型方法call()apply()bind(),对this进行显示绑定。
首先我们先对三个方法的概念下手,如下是参考的相关文档整理的:

①、call()

定义:
call()方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

语法:

fun.call(thisArg, arg1, arg2, ...)

参数:

  • thisArg:在 fun 函数运行时指定的 this 值。if(thisArg == undefined|null) this = window,if(thisArg == number|boolean|string) this == new Number()|new Boolean()| new String()
  • arg1, arg2, …: 指定的参数列表

返回值: 使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

②、apply()

定义:
apply()方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。

语法:

func.apply(thisArg, [argsArray])

参数:

  • thisArg:可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
  • argsArray: 可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。

返回值: 调用有指定this值和参数的函数的结果

③、bind()

定义:
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的thisbind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。

语法:

function.bind(thisArg[,arg1[,arg2[, ...]]])

参数:

  • thisArg:调用绑定函数时作为this参数传递给目标函数的值。如果bind函数的参数列表为空,执行作用域的this将被视为新函数的thisArg。
  • arg1, arg2, …: 当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。

返回值: 返回一个原函数的拷贝,并拥有指定的this值和初始参数。

概念介绍完了,上代码:

// call 与 apply

function foo () {
    
    
    console.log(this + 'foo函数被调用了')
}
var obj = {
    
    
    name: 'obj'
}
// 调用方法一:直接调用 指向的是全局对象(window)
foo()

// 调用方法二:  call和apply与直接调用不同的在于this的绑定不同 ,可以直接指定this的绑定对象为obj

foo.call(obj) //{name: 'obj'} 
foo.apply(obj) //{name: 'obj'}

// bind
function bar () {
    
    
    console.log(this)
}

var newBar = bar.bind('aaa')
newBar() // String {'aaa'}   解释一下 newBar()句话 很明显是直接调用应该指向全局对象window 但是bind显示绑定的优先级更高 (优先级会在下一篇文章更新)

4、new绑定

在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。包括内置对象函数在内的所有函数都可以用new来调用,这种函数调用称为构造函数调用。
我们通过一个new关键字调用一个函数时(构造器),这个时候this是在调用这个构造器时创建出来的对象。 this 等于 创建出来的对象 ,这个过程就是 new 绑定。

示例代码如下:

function Person (name, sex) {
    
    
    this.name = name
    this.sex = sex
}

var p1 = new Person('张三', '男')
console.log(p1.name, p1.sex) // 张三 男
// 每次都会创建新对象赋值给this
var p2 = new Person('李红', '女')
console.log(p2.name, p2.sex) // 李红 女

5、call和apply的区别

根据上面的概念我们可以很明显的看出来,两个方法接收的参数不一样,其实作用基本一致。

// call 与 apply的区别是什么?
function sum (num1, num2, num3) {
    
    
    console.log(num1 + num2 + num3)
}
sum.call('call', 10, 20, 30) // 60
sum.apply('apply', [10, 20, 30]) // 60
// call参数是单个的值,而apply的参数是一个数组,就这点差别

猜你喜欢

转载自blog.csdn.net/qq_49002903/article/details/124394236