(JS)15分钟带你彻底跨越JS的一座大山--this

341 阅读7分钟

前言

在JS的学习过程中,this是我们不可忽略的一个非常重要的关键字,许多企业级开发的代码中都会大量使用它,许多人都称之为JS的一座大山。本文将引用大量实例带大家彻底搞懂this这个关键字,理清楚this的使用规则。

this是一个对象吗

new一个构造函数创建实例对象的过程中,我们知道,构造函数中会创建一个this对象。那么this真的是一个对象吗?我们可以在浏览器上打印一下this:

屏幕截图 2023-11-17 083501.png

你会发现this其实代指的是一个window对象,注意我说的是代指。this本身不是一个对象,this 关键字的值取决于它在哪个上下文中使用。

当在全局作用域中使用时,this 指向全局对象,通常在浏览器环境中是 window 对象。在函数内部使用时,this 的值取决于函数的调用方式。总体来说,this 的值是在运行时动态确定的,它取决于函数被调用的方式。因此,this 不是固定的对象,而是在特定上下文中引用的对象。

this究竟有什么用

我可以直接告诉你,this就是用来简化代码的。那么我们先看一段代码:

function identify(){
    return this.name
}
function speak(){
    var friend = identify.call(this)
    console.log(friend);
}
var me = {
    name = 'Bill'
}
speak.call(me)

上述代码就是一个函数的传参问题。我们会发现,打印出来的是Bill。而原代码并没有传参过程。这里其实就是this实现的了一个隐式传参,也就是我们看不到的传参过程。 这样使我们可以通过this在函数体内部便捷的引用运行环境中的变量而不需要传参,极大的方便了开发。

那么this的这个匪夷所思功能背后的 真相究竟是什么?

this的绑定规则

我们是不是在this的引用过程中会发现,this最关键的地方就是它究竟代指的是谁?那么这里就要引入一个新的概念——this的绑定。那么this的绑定种类有4种:

1.默认绑定

默认绑定是多用于函数中的,就比如上述的函数的传参。规则也很简单: 函数在哪个词法作用域里生效,this就指向哪里的作用域,最终指向全局。

var a=1

function foo(){
    console.log(this.a);
}
foo()

输出结果是1 。那么很明显这里this代指的是全局作用域,foo函数在全局里被调用,全局又定义了一个值为1的a变量。

—— 那如果一个函数在另一个函数体内调用呢

 function foo(){
     var a =2;
     this.bar()
 }
 function bar() {
     console.log(this.a); 
 }
 foo()

这里打印的是undefined。这里的this.a拿不到值,你会发现很奇怪,bar函数不是在foo中被调用了吗,所以this代指的是foo中的a?答案是this没有代指foo的作用域,而是代指了全局。在es6中,this有一个硬性规则:this无法访问一个词法作用域内部的内容。所以this在foo中拿不到a的值,又会顺着去找foo的词法作用域——全局。而全局没有定义a变量,所以是undefined

2.隐式绑定

成立于:当函数被一个对象所拥有(引用),且再被调用时此时this会指向该 对象,该对象就是函数调用时的上下文。而对象没有作用域,这也说明this不一定要代指含一个作用域

//定义一个函数 且在一个对象中分别调用和引用
function foo(){
    console.log(this.a);
}
var obj = {
    a : 2,
    foo: foo, //引用(拥有)
    foo1:foo() //调用
}
obj.foo()
console.log(obj.foo1);

上述会有两个打印值:2 和 undefined。很明显,obj 中 foo:foo完成对函数的引用,而 foo1:foo( )是调用。所以我们在隐式绑定的过程中,一定要区分清楚函数的引用和调用

——而隐式绑定中还有一个特殊情况:隐式丢失

function foo(){
    console.log(this.name);
}
var obj = {
    name :'Alice',
    foo:foo
}
var say = obj.foo // 函数引用被赋值给变量 say()
say()

这里的打印的是undefined。在这个例子中,name 函数在 obj 上被定义,正常情况下,this 应该绑定到 obj,输出"Alice"。然而,将 name 赋值给 say 变量后,say 在全局上下文中被调用,此时 this 的绑定丢失,指向了全局,输出了 undefined。这是因为在全局上下文中,this 默认指向 window(浏览器环境)或 global(Node.js 等环境),而全局对象上并没有 name 属性。

3.显示绑定

显示绑定中会用到三种内置的方法:call方法 , apply方法 , bind方法。

显示绑定允许你明确地指定一个对象作为函数调用时的上下文,而不依赖于默认的绑定规则,非常方便于开发。

——使用 call 方法:

var obj = { name: '小花' }
function sayName() { 
    console.log('My name is ' + this.name); 
} 
sayName.call(obj) // 输出 "My name is 小花"

在上面的例子中,call 方法被用于将 sayName 函数与不同的对象进行显式绑定,确保 this 代指传递给 call 方法的对象。

——callapplybind的区别:

var obj = { name: 'Alice' }; 
function sayName(greeting) 
{ 
    console.log(greeting + ' My name is ' + this.name); 
}
sayName.call(obj,'Hello')
sayName.apply(obj, ['Hello']) 
var bar = sayName.bind(obj, 'Hello') 
bar()

上述代码会输出三"Hello My name is Alice",三者绑定的规则类似,但是传参的形式不同。

call接受一个普通的参数列表, apply接受一个参数数组而不是单独的参数列表。而bind会返回一个新的函数,该函数的 this 被绑定到指定的对象。

4.new绑定

new 绑定是一种用于创建新对象的特殊绑定规则。当一个函数通过 new 关键字来调用时,称之为构造函数调用,此时会创建一个新的对象,并将这个新对象绑定到函数调用中的 this 上。

下面是一个使用 new 绑定的简单例子:

function Person(name) { 
    // 在使用 new 时,this 将指向新创建的对象 
    this.name = name; 
} 
var alice = new Person('Alice'); 
console.log(alice.name); // 输出 "Alice"

在这个例子中,Person 是一个构造函数,通过 new Person('Alice') 创建了一个新的对象 alice。在构造函数内部,this.name 将成为新对象 alice 的属性。

实际上:使用 new 关键字时,构造函数内部发生了以下几个步骤:

  1. 创建一个新的空对象。
  2. 将新对象的内部 [[Prototype]] 属性(指向构造函数的原型对象)设置为构造函数的 prototype 属性。
  3. 将构造函数内部的 this 绑定到新创建的对象上。
  4. 执行构造函数内部的代码,对新对象进行初始化操作。
  5. 如果构造函数没有显式返回一个对象,则返回新创建的对象。

那么需要注意的是,new 绑定是优先级最高的,如果同时存在多种绑定规则,new 绑定会覆盖其他规则。

this与箭头函数

var bar = () => {
    //函数体
}

箭头函数是是es6新增的一种新的函数写法,省略了一些代码,例如不用写function。而箭头函数在this的使用方面又有不同的地方:

箭头函数没有自己的 this 绑定或者说它没有this属性,相当于this写在了箭头函数外部,它会继承父作用域中的 this

function cool() { 
    this.value = 1;
    setTimeout( () => { 
        // 箭头函数继承了父作用域中的 this 
        this.value++; 
        console.log(this.value); // 输出 2
    }, 1000); 
}
cool() 

这使得箭头函数在一些情况下更易于理解和使用,避免了传统函数中 this 绑定可能导致的问题。

总结

文章参考:《你不知道的JavaScript》

有任何想法和建议欢迎大家在评论区留言哦~

点个免费的赞鼓励支持一下吧! 个人Gitee仓库:Code Space: 记录学习code中的点点滴滴 (gitee.com)