首页 > 其他分享 >【JS基础】this的三两事

【JS基础】this的三两事

时间:2024-11-11 14:48:41浏览次数:3  
标签:函数 对象 猫咪 绑定 基础 JS call var 三两

this是JavaScript语法中的一个关键字,为什么要用this呢?它能做什么呢?为什么很多前辈和一些书籍,文章中都会告诉我:

this是个重点,稍有不慎就会搞错。

真的吗?好吧,那我也学学看~

01. 我遇到的问题

我见到过的this,一般出现在函数中。这让我很容易见文会意.它是不是表示:当前这个函数。this嘛?翻译过来不就是这个的意思,所以它就表示当前的这个函数。

这是我最初的理解。哎呀,这有什么难的?看来他们说的危言耸听了吧。

接下来,我自己写一些测试代码,再加深一下对this的认识。

var a = 'global'
var b = 0

function test1(paramA, paramB) {
    var a = 'inner'
    var b = 1
    
    console.log(this.a)
    console.log(this.b)
}

test1()

像上面的代码,在最外层和test1函数中都声明了相同名称的变量:ab 代码最后一行调用函数时,执行里面的代码,要打印的东西分别是this.athis.b.此时是在函数内部,那么this毫无疑问应该指向test1函数作用域的。

自然得到的结果是:inner和1

然而,让我大跌眼镜的是,居然输出的global和0

啊!为什么呀?跟外面的a,b有什么关系?

就这样由一个不能理解的问题入手,我开始了this的学习之旅。

02. this的理解

我是一名coder,在学了很多东西后,自然的形成了一种认识: 每一个技术,都不会凭空(无缘无故)的产生,它的出现理应是为了解决一些问题的,并且有自己的适用范围。

因此,关于this我也是这么考虑的。

首先,我其实到现在都很疑惑,到底JavaScript算是一门面向对象的编程语言呢?还是,有观点也认为它其实是面向函数的语言。甚至说它其实两者都不是。

如此种种,算了我也不再细究。可能是这门语言,不是那么的纯粹。所以,导致如果我们从不同的视角来看,它都有可能符合其中的一些特性。

this我认为则是从面向对象这个视角下,存在的一种机制。

为什么要有this

既然现在是把JS当成一门面向对象的语言,那么在设计代码解决问题时,自然会把所有东西都看成是一个个的对象。

基于对象,我们会对其进行静态的属性描述,以及动态的行为上的设置。而这些行为,我们又一般会通过一个个的函数来呈现。

// 定义一个猫咪的对象
var cat = {
    name: '猫咪', // 名称
    callDesc: '喵~喵~', // 猫咪是怎么叫的
    call: function() { // 描述猫咪叫的行为
       console.log(`${cat.name}的叫声是:${cat.callDesc}`) 
    }
}

// 定义一个小狗的对象
var dog = {
    name: '小狗',
    callDesc: '汪~汪~',
    call: function() {
        console.log(`${dog.name}的叫声是:${dog.callDesc}`) 
    }
}

上面的代码,我们分别定义了一个cat、dog的对象,其内部都有名称,以及叫声的描述和行为。哪怕是现在只有两个对象,也能看到call这个方法,我们完全可以抽出去形成一个单独的函数,这样后续哪怕再添加其他对象,就都可以复用了。

// 可复用的“动物叫”这个行为的函数
function animalCall(context) {
    console.log(`${context.name}的叫声是:${context.callDesc}`) 
}

// 这样的话,上面的cat、dog对象,就可以变成这样了
var cat = {
    name: '猫咪', // 名称
    callDesc: '喵~喵~', // 猫咪是怎么叫的
    call: animalCall(cat)
}
var dog = {
    name: '小狗',
    callDesc: '汪~汪~',
    call: animalCall(dog)
}

cat.call() // 输出,猫咪是怎么叫的
dog.call() // 输出,小狗是怎么叫的
// 可复用的“动物叫”这个行为的函数
function animalCall(context) {
    console.log(`${context.name}的叫声是:${context.callDesc}`) 
}

// 这样的话,上面的cat、dog对象,就可以变成这样了
var cat = {
    name: '猫咪', // 名称
    callDesc: '喵~喵~', // 猫咪是怎么叫的
    call: animalCall(cat)
}
var dog = {
    name: '小狗',
    callDesc: '汪~汪~',
    call: animalCall(dog)
}

cat.call() // 输出,猫咪是怎么叫的
dog.call() // 输出,小狗是怎么叫的

但是上面的代码还是存在一些问题,比如我们在使用animalCall这个函数的时候,还需要给它传当前的对象。这样一是繁琐,二是在后面如果要给对象重起个名字,这块还得改。

所以,就有了一种更优雅的方式。也就是使用this

// 使用this,定义可复用的函数
function animalCall() {
    console.log(`${this.name}的叫声是:${this.callDesc}`) 
}

// 这样的话,使用animalCall函数时,就可以什么都不用传了。
// 哪怕后面cat、dog名称修改了,也不需要再修改其他的东西了
var cat = {
    name: '猫咪', // 名称
    callDesc: '喵~喵~', // 猫咪是怎么叫的
    call: animalCall()
}
var dog = {
    name: '小狗',
    callDesc: '汪~汪~',
    call: animalCall()
}

cat.call() // 输出,猫咪是怎么叫的
dog.call() // 输出,小狗是怎么叫的

当使用了this后,使得代码会更加的简洁和优雅。

这时候,我就不禁有这样的思考:

之前对this的误解,是不是我只看到了它出现在函数中,刻板的把它理解成了函数的某种形式。因此,它的表示逃离不出函数的范围:比如误解成this指向函数自身;指向函数的作用域。

然而,并非如此。this它是基于对象下的对其行为描述的一种设计机制。

那么它其实就跟使用它的对象有关。

03. this的工作方式

平时,我们定义的函数,一般会被放到堆内存中。当要调用它时,会把它压到一个执行栈中去运行,与此同时会创建一个执行记录,来协助函数的执行。里面会包含一些现场信息,而this此时也才会被创建,并包含在其中。

所以,我们看到this是在运行时被创建的,它的具体指向是与调用位置有关。

对,没错。就是调用位置

而绝不是声明位置!不是!不是!

绑定规则

那么根据调用位置的不同,this是有如下四种的绑定规则:

  1. 默认绑定
  2. 隐式绑定
  3. 显式绑定
  4. new绑定

下面,就对其一一进行讲解:

默认绑定

它是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。 ——来源自《你不知道的JavaScript上卷》

什么样的调用算是独立函数调用呢?

function test2() {
    console.log(this.a)
}

var a = 1

test2() // 1

如上面的代码所示,就是调用test2函数时,没有任何的修饰。此时在函数中应用的就是this的默认绑定

注意

在应用默认绑定时,this的具体指向还会跟代码是否处于严格模式下有关联。

简单的说,就是:

  • 严格模式下,不能将全局对象用于默认绑定,此时this会绑定到undefined
  • 非严格模式下,this就绑定到全局对象下
隐式绑定

当函数调用时,看它是否被包含在某个对象下。如果是的话,那么此时this就会应用隐式绑定,把它绑定到那个对象下。

比如:

var test03Obj = {
    a: 1,
    b: test03Fn
}

function test03Fn() {
    console.log(this.a)
}

test03Obj.b() // 1

test03Fn这个函数是作为test03Obj这个对象下b属性的值,当我们用test03Obj.b()方式调用时,就使用了隐式绑定,此刻this指向的就是test03Obj这个对象。

注意

在这些情况下,隐式绑定会丢失,此时变成了默认绑定

  1. 函数别名
  2. 作为函数的参数进行传递
var a = 'global'

function test04() {
    console.log(this.a)
}

var test04Obj = {
    a: 'inner',
    b: test04
}

// 把函数赋值给一个变量(相当于给函数起了个别名)
var newNameFn = test04Obj.b

newNameFn() // 'global'

在13行代码中,我们看到是把test04Obj.b这个函数赋值给了newNameFn,它现在是test04Obj.b的一个引用。但实际上,它指向的还是test04这个函数本身。

而我们在15行去调用这个newNameFn是完全不带任何的修饰,此时它就变成了默认绑定。输出的结果自然就是global。

var a = 'global'

function test05() {
    console.log(this.a)
}

var test05Obj = {
    a: 'inner',
    b: test05
}

function handlerTest05(callback) {
    callback()
}

handlerTest05(test05Obj.b) // 'global'

在第16行时,test05Obj.b是作为一个参数被传递到了handlerTest05这个函数中,并且在这个函数内部,也会执行这个作为回调的,也就是test05这个函数。

然而,我们看到的结果,依然是输出了全局对象下的变量a的值。

所以,通过以上两个例子,依然是更能说明:this的绑定,是只跟调用位置有关。

哪怕这个过程中涉及到一些隐式绑定的使用方式,但它们都只是作为一个中间环节,服务于最终要调用的那个函数。

而那个最终的函数的调用位置是什么,就决定着this最终的指向。

显式绑定

有些时候,如果我们希望this能确定的绑定到某个对象下,那么就可以使用显式绑定。

到目前为止,可以使用函数自身提供的方法:

  1. call()
  2. apply()
  3. 以及bind()

来强制的把this绑定到某个对象下。

var a = 'global'

function test06() {
    console.log(this.a)
}

var test06Obj = {
    a: 'inner',
    b: test06
}

test06.call(test06Obj) // 'inner'

// 或者是
test06.apply(test06Obj) // 'inner'

// 使用bind
var bindOfTest06 = test06.bind(test06Obj)
bindOfTest06() // 'inner'
new绑定

在了解new绑定之前,需要先明确一点。它和其他的面向对象编程语言中的使用new关键字,来实例化一个类是完全不同的。

在JavaScript中,它其实很简单。甚至可以说相当简陋和朴素。

它只是被new操作符调用的普通函数而已。

不信我们看,在使用new操作符来调用函数时,会自动执行下面的动作:

  1. 创建一个全新的对象
  2. 这个新对象会被执行[[Prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function test07(a) {
    this.a = a
}

var newObj = new test07('new')
console.log(newObj.a) // 'new'

上面的代码可以理解为,当使用new操作符来调用函数时,会返回一个新的对象给变量newObj,而这个新对象中包含了一个值为new的a属性。

绑定规则的优先级

this的绑定规则是有上面的四种,但是当某个调用位置可以使用多种调用规则时,this又会指向什么呢?

关于这个问题,就不得不考虑绑定规则之间的优先级了。

todo:示例代码后续补充……

结论则是:

new绑定 > 显式绑定(apply、call、bind)> 隐式绑定 > 默认绑定

所以,当我们判断this的绑定时,可以按照如下顺序进行判断:

  1. 函数是否在new中调用?如果是的话,this绑定的就是新创建的对象
  2. 函数是否通过call、apply或者bind调用?如果是的话,this绑定的就是指定的对象
  3. 函数是否在某个上下文对象中调用?如果是的话,this绑定的就是那个上下文对象
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象
绑定的例外情况(后续补充)

this的词法(箭头函数)

在ES6中,我们新推出了一种定义函数的方式:就是箭头函数(它不是使用function关键字来声明的,而是用了被称为“胖箭头”的操作符的方式)

可能在此之前,开发者们苦this的复杂易错的绑定机制,久矣!

于是,JavaScript新的标准,从语言层面上就帮我们避开了这些问题。

箭头函数并不会应用上面讲到的四种绑定规则,而是根据当前的词法作用域来决定this。具体来说,就是会继承外层函数调用的this绑定。 —— 来自《你不知道的JavaScript上卷》

浅浅的总结一下

标签:函数,对象,猫咪,绑定,基础,JS,call,var,三两
From: https://blog.csdn.net/2401_87546826/article/details/143507797

相关文章