百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 行业知识 > 正文

带你了解JavaScript中的“this”

citgpt 2024-11-25 10:02 4 浏览 0 评论

this是什么?

JavaScript 中最令人困惑的机制之一就是 this 关键字。它是一个在每个函数作用域中自动定义的特殊标识符关键字,但即便是一些老练的 JavaScript 开发者也对它到底指向什么感到困扰。

我们要明白,this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。

带你了解JavaScript中的“this”

那该如何来判断函数的执行是如何绑定的this?上面已经说明,this与函数被调用的方式紧密相连,那这个被调用的方式就是一个判断的调用点。

调用点(Call-site)

调用点:函数在代码中被调用的位置(而不是被声明的位置)。一般来说寻找调用点就是:“找到一个函数是在哪里被调用的”,但它不总是那么简单,比如某些特定的编码模式会使 真正的 调用点变得不那么明确。

考虑 调用栈(call-stack) (使我们到达当前执行位置而被调用的所有方法的堆栈)是十分重要的。我们关心的调用点就位于当前执行中的函数 之前 的调用。

以下代码展示调用栈和调用点

function test1() {
    // 调用栈是: `test1`
    // 我们的调用点是 global scope(全局作用域)

    console.log( "test1" );
    test2(); // <-- `test2` 的调用点
}

function test2() {
    // 调用栈是: `test1` -> `test2`
    // 我们的调用点位于 `test1`

    console.log( "test2" );
    test3(); // <-- `test3` 的 call-site
}

function test3() {
    // 调用栈是: `test1` -> `test2` -> `test3`
    // 我们的调用点位于 `test2`

    console.log( "test3" );
}

test1(); // <-- `test1` 的调用点

从代码中分析寻找调用点时要小心,因为它是影响 this 绑定的唯一因素。

默认绑定(Default Binding)

第一种规则最常见的就是:独立函数调用。可以认为这种 this 规则是在没有其他规则适用时的默认规则。

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

var a = 2;

foo(); // 2

第一点: 在全局作用域中的声明变量var a = 2,是全局对象的同名属性的同义词。它们不是互相拷贝对方,它们就是彼此。

第二点: 我们看到当foo()被调用时,this.a 解析为我们的全局变量 a。为什么?因为在这种情况下,对此方法调用的 this 实施了 默认绑定,所以使 this 指向了全局对象。

我们怎么知道这里适用 默认绑定 ?我们考察调用点来看看 foo() 是如何被调用的。在我们的代码段中,foo() 是被一个直白的,毫无修饰的函数引用调用的。没有其他的我们将要展示的规则适用于这里,所以 默认绑定 在这里适用。

如果 strict mode 在这里生效,那么对于默认绑定来说全局对象是不合法的,所以 this 将被设置为 undefined

function foo() {
    "use strict";

    console.log( this.a );
}

var a = 2;

foo(); // TypeError: `this` is `undefined`

隐含绑定(Implicit Binding)

另一种规则是:调用点是否有一个环境对象(context object),也称为拥有者(owning)或容器(containing)对象。

考虑这段代码:

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

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

注意: foo() 被声明然后作为引用属性添加到 obj 上的方式。无论 foo() 是否一开始就在 obj 上被声明,还是后来作为引用添加(如上面代码所示),这个 函数 都不被 obj 所真正“拥有”或“包含”。

然而,调用点 使用 obj 环境来 引用 函数,所以你 可以说 obj 对象在函数被调用的时间点上“拥有”或“包含”这个 函数引用

不论你怎样称呼这个模式,在 foo() 被调用的位置上,它都是一个指向 obj 的对象引用。当一个方法引用存在一个环境对象时,隐含绑定 规则会说:是这个对象应当被用于这个函数调用的 this 绑定。

因为objfoo() 调用的 this,所以 this.a 就是 obj.a 的同义词。

只有对象属性引用链的最后一层是影响调用点的。比如:

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

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42

隐含丢失(Implicitly Lost)

this 绑定最常让人困惑的事情之一,就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据 strict mode 的状态,其结果不是全局对象就是undefined

考虑这段代码:

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

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函数引用!

var a = "oops, global"; // `a` 也是一个全局对象的属性

bar(); // "oops, global"

调用点是 bar(),一个直白,毫无修饰的调用,因此 默认绑定 适用于这里。

当我们考虑传递一个回调函数时,会怎样了?

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

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

setTimeout( obj.foo, 100 ); // "oops, global"

没有区别,同样的结果。

正如我们刚刚看到的,我们的回调函数丢掉他们的 this 绑定是十分常见的事情。

不管哪一种意外改变this 的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你(还)没有办法控制调用点给你一个故意的绑定。我们很快就会看到一个方法,通过 固定 this 来解决这个问题。

明确绑定(Explicit Binding)

如果我们想强制一个函数调用使用某个特定对象作为 this 绑定,而不在这个对象上放置一个函数引用属性呢?

函数都有一些工具: 拥有call(..)apply(..) 方法。它们接收的第一个参数都是一个用于 this 的对象,之后使用这个指定的 this 来调用函数。因为你已经直接指明你想让 this 是什么,所以我们称这种方式为 明确绑定(explicit binding)

考虑这段代码:

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

var obj = {
    a: 2
};

foo.call( obj ); // 2

通过 foo.call(..) 使用 明确绑定 来调用 foo,允许我们强制函数的 this 指向 obj

注意:this 绑定的角度讲,call(..)apply(..) 是完全一样的。它们确实在处理其他参数上的方式不同,但那不是我们当前关心的。

new 绑定(new Binding)

第四种也是最后一种 this 绑定规则,要求我们重新思考 JavaScript 中关于函数和对象的常见误解。

在传统的面向类语言中,“构造器”是附着在类上的一种特殊方法,当使用 new 操作符来初始化一个类时,这个类的构造器就会被调用。通常看起来像这样:

something = new MyClass(..);

JavaScript 拥有 new 操作符,而且使用它的代码模式看起来和我们在面向类语言中看到的基本一样;大多数开发者猜测 JavaScript 机制在做某种相似的事情。但是,实际上 JavaScript 的机制和 new 在 JS 中的用法所暗示的面向类的功能 没有任何联系。

首先,让我们重新定义 JavaScript 的“构造器”是什么。在 JS 中,构造器 仅仅是一个函数,它们偶然地与前置的 new 操作符一起调用。它们不依附于类,它们也不初始化一个类。它们甚至不是一种特殊的函数类型。它们本质上只是一般的函数,在被使用 new 来调用时改变了行为。

当在函数前面被加入 new 调用时,也就是构造器调用时,下面这些事情会自动完成:

  1. 一个全新的对象会凭空创建(就是被构建)
  2. 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  3. 这个新构建的对象被设置为函数调用的 this 绑定
  4. 除非函数返回一个它自己的其他对象,否则这个被 new 调用的函数将 自动 返回这个新构建的对象。

考虑这段代码:

function foo(a) {
    this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

通过在前面使用 new 来调用 foo(..),我们构建了一个新的对象并把这个新对象作为 foo(..) 调用的 thisnew 是函数调用可以绑定 this 的最后一种方式,我们称之为 new 绑定(new binding)


一切皆有顺序

如此,我们已经揭示了函数调用中的四种 this 绑定规则。你需要做的 一切 就是找到调用点然后考察哪一种规则适用于它。但是,如果调用点上有多种规则都适用呢?这些规则一定有一个优先顺序,我们下面就来展示这些规则以什么样的优先顺序实施。

很显然,默认绑定 在四种规则中优先权最低的。所以我们先把它放在一边。

隐含绑定明确绑定 哪一个更优先呢?我们来测试一下:

function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

所以, 明确绑定 的优先权要高于 隐含绑定,这意味着你应当在考察 隐含绑定 之前 首先 考察 明确绑定 是否适用。

现在,我们只需要搞清楚 new 绑定 的优先级位于何处。

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

好了,new 绑定 的优先级要高于 隐含绑定。那么你觉得 new 绑定 的优先级较之于 明确绑定 是高还是低呢?

注意newcall/apply 不能同时使用,所以 new foo.call(obj1) 是不允许的,也就是不能直接对比测试 new 绑定明确绑定。但是我们依然可以使用 硬绑定 来测试这两个规则的优先级。

在我们进入代码中探索之前,回想一下 硬绑定 物理上是如何工作的,也就是 Function.prototype.bind(..) 创建了一个新的包装函数,这个函数被硬编码为忽略它自己的 this 绑定(不管它是什么),转而手动使用我们提供的。

因此,这似乎看起来很明显,硬绑定(明确绑定的一种)的优先级要比 new 绑定 高,而且不能被 new 覆盖。

我们检验一下:

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

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

哇!bar 是硬绑定到 obj1 的,但是 new bar(3) 并 没有 像我们期待的那样将 obj1.a 变为 3。反而,硬绑定(到 obj1)的 bar(..) 调用 可以 被 new 所覆盖。因为 new 被覆盖,我们得到一个名为 baz 的新创建的对象,而且我们确实看到 baz.a 的值为 3

为什么 new 可以覆盖 硬绑定 这件事很有用?

这种行为的主要原因是,创建一个实质上忽略 this硬绑定 而预先设置一部分或所有的参数的函数(这个函数可以与 new 一起使用来构建对象)。bind(..) 的一个能力是,任何在第一个 this 绑定参数之后被传入的参数,默认地作为当前函数的标准参数(技术上这称为“局部应用(partial application)”,是一种“柯里化(currying)”)。

例如:

function foo(p1,p2) {
    this.val = p1 + p2;
}
// 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
// 而且反正它将会被 `new` 调用覆盖掉!
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2

判定 this

现在,我们可以按照优先顺序来总结一下从函数调用的调用点来判定 this 的规则了。按照这个顺序来问问题,然后在第一个规则适用的地方停下。

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。var bar = new foo()
  2. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。var bar = foo.call( obj2 )
  3. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。var bar = obj1.foo()
  4. 否则,使用默认的 this默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。var bar = foo()

以上,就是理解对于普通的函数调用来说的 this 绑定规则。

相关推荐

外贸网站建设要多少钱?外贸网站建设周期要多久

大家好,我是【无锡柠萌网络lemon56.com】的小美,今天分享:外贸网站建设要多少钱?外贸网站建设周期要多久您关注的问题·FAQ外贸网站建设中遇到的常见问题,让您少走弯路,提高效率!...

500块搭建一个可以卖货的跨境电商独立站,包括服务器和域名吗?

用500元搭建一个可以卖货的跨境电商独立站,虽然有一定挑战性,但通过精打细算和选择合适的工具和服务,仍然是有可能的。以下是一些建议,帮助你在预算范围内实现这个目标,包括服务器和域名的选择。...

无锡网络公司设计搭建阀门网站一般要多少钱?

无锡网络公司设计搭建阀门网站一般要多少钱?这是一个涉及多方面因素的复杂问题,其费用因网站规模、功能需求、设计复杂度及后续维护等因素而异。首先,从基础成本出发,域名注册是搭建网站的第一步,费用通常在几十...

定制化网站开发大概多少钱

定制化网站开发的价格因项目的复杂性、功能需求、设计要求和开发时间等因素而异。以下是一些常见的定制化网站开发价格范围,供参考:1.简单的定制化网站开发:一般来说,一个简单的定制化网站开发项目可能需要花...

9月安卓手机性能排行榜出炉:前十名差距仅为7%

时间已经进入9月,按照安兔兔目前的安卓手机性能榜单显示,红魔9SPro+依旧力压其他热销手机成为榜首,可见游戏定位的这款手机,在性能上确实非常的出色。不过,笔者也发现了一个问题,其中前十名中得分最高...

装修公司怎么在网络平台接单?实现高效获客

装修行业正经历着深刻的变革,随着消费者需求的日益多样化与个性化,传统的线下获客方式已难以满足装修公司的业务需求。因此,装修公司必须紧跟时代步伐,充分利用网络平台实现高效获客。1、入驻装修接单平台:壹品...

企业如何利用二维码进行线上营销?

#企业如何利用二维码进行线上营销?#企业利用二维码进行线上营销,可以从以下几个方面入手:?1.设计创意二维码?:...

家电销售该怎么线上拓客

社交媒体平台:利用微信、微博、抖音等平台,定期发布家电产品的信息、使用心得、促销活动等内容。引流靠手动一定是不行的,所以一般使用点软件肯定没错,最近用了款比较冷门的APP,"里德助手Plus...

pop社交软件,脱单软件排行榜

在交友软件里面脱单是非常明确的一件事,各年龄各社交平台上的女人都是有所不同的,具体问题具体分析一下。接下来就说说又哪些比较优质的恋爱脱单软件。...

手机处理器最新跑分排行榜,你的手机什么水平?

手机处理器最新跑分排行榜,你的手机什么水平?第1名:天玑9300第2名:骁龙8Gen3第3名:A17Pro第4名:天玑9200第5名:骁龙8Gen2第6名:骁龙8Gen1...

继番茄小说后,字节再推免费网文 App“蛋花小说”和“常读小说”

据Tech星球报道,字节跳动公司近期推出了两款全新的免费网文App,分别是“蛋花小说”和“常读小说”。这两款产品的开发公司分别为湖北福瑞兴网络科技有限公司和湖北聚合润网络科技有限公司,均为字节跳动的1...

全世界最好用的AI软件排名是?

Hey小伙伴们,今天咱们来聊聊那些让人爱不释手的AI神器!在这个智能化时代,谁还没几个拿得出手的AI软件呢?别急,我这就给你盘点一波全球超火的AI软件,保证让你大开眼界!...

手机root软件哪个成功率高?手机root软件排行榜2025

作为游戏玩家,欲在手游中畅玩无阻,root权限不可或缺,可优化画质、启用辅助。2025年已至,究竟哪些手机root软件能脱颖而出?哪款软件的root成功率会独占鳌头?一、手机root软...

独立站新手教程引流篇:如何优化谷歌广告投放效果?

随着谷歌广告单价的持续上涨,如何在提升投放效果的同时,降低推广费用成为每一个独立站卖家的必修课。因此新手卖家在完成初步的广告投放流程后,最重要的就是了解一下谷歌广告优化的基础策略。设置转化跟踪即利用G...

云媒易:干货知识分享!海外推广的渠道有哪些,如何正确的选择?

越来越多的海外企业或国内的跨境商家认识到海外的网络营销的作用,开展线上外贸营销,渠道是非常的重要,那么海外的推广渠道有哪些?企业应该如何选择呢?1、社交媒体推广社交媒体推广是现在海外推广方式中最热门的...

取消回复欢迎 发表评论: