无人不识又无人不迷糊的this
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
本文分享自华为云社区《3月阅读周·你不知道的JavaScript | 无人不识又无人不迷糊的this》,作者: 叶一一。 关于thisthis关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。 为什么要用this随着开发者的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用this则不会这样。 比如下面的例子: function identify() { return this.name.toUpperCase(); } function speak() { var greeting = "Hello, I'm " + identify.call(this); console.log(greeting); } var me = { name: 'Kyle', }; var you = { name: 'Reader', }; console.log(identify.call(me)); console.log(identify.call(you)); speak.call(me); speak.call(you); 打印一下结果: 上面的代码可以在不同的上下文对象(me和you)中重复使用函数identify()和speak(),不用针对每个对象编写不同版本的函数。如果不使用this,那就需要给identify()和speak()显式传入一个上下文对象。 误解有两种常见的对于this的解释,但是它们都是错误的。 1、指向自身 人们很容易把this理解成指向函数自身。 那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。 看下面这段代码,思考foo会被调用了多少次? function foo(num) { console.log('foo: ' + num); // 记录foo被调用的次数 this.count++; } foo.count = 0; var i; for (i = 0; i < 10; i++) { if (i > 5) { foo(i); } } // foo被调用了多少次? console.log(foo.count); 打印结果: console.log语句产生了4条输出,证明foo(..)确实被调用了4次,但是foo.count仍然是0。显然从字面意思来理解this是错误的。 执行foo.count = 0时,的确向函数对象foo添加了一个属性count。但是函数内部代码this.count中的this并不是指向那个函数对象。 2、它的作用域 第二种常见的误解是,this指向函数的作用域。这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下它却是错误的。 this在任何情况下都不指向函数的词法作用域。 function foo() { var a = 2; this.bar(); } function bar() { console.log(this.a); } foo(); 直接打印上面的代码会得到一个报错: 这段代码试图通过this.bar()来引用bar()函数。这是不可能实现的,使用this不可能在词法作用域中查到什么。 每当开发者想要把this和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。 this到底是什么this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。 this全面解析调用位置在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。 寻找调用位置就是寻找“函数被调用的位置”。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中。 通过下面的代码来看什么是调用栈和调用位置: function baz() { // 当前调用栈是:baz // 因此,当前调用位置是全局作用域 console.log('baz'); bar(); // <-- bar的调用位置 } function bar() { // 当前调用栈是baz -> bar // 因此,当前调用位置在baz中 console.log('bar'); foo(); // <-- foo的调用位置 } function foo() { // 当前调用栈是baz -> bar -> foo // 因此,当前调用位置在bar中 console.log('foo'); } baz(); // <-- baz的调用位置 打印的结果如下: 绑定规则来看看在函数的执行过程中调用位置如何决定this的绑定对象。 首先必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。 充分理解四条规则之后,再理解多条规则都可用时它们的优先级如何排列。 1、默认绑定 首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。 var a = 2; function foo() { console.log(this.a); } foo(); // 2 打印结果是2。也就是当调用foo()时,this.a被解析成了全局变量a。函数调用时应用了this的默认绑定,因此this指向全局对象。 2、隐式绑定 另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。 思考下面的代码: function foo() { console.log(this.a); } var obj = { a: 2, foo: foo, }; obj.foo(); // 2 当foo()被调用时,它的前面确实加上了对obj的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。 3、显式绑定 JavaScript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..)和apply(..)方法。 它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。 因为可以直接指定this的绑定对象,因此我们称之为显式绑定。 思考下面的代码: function foo() { console.log(this.a); } var obj = { a: 2, }; foo.call(obj); // 2 通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。 4、new绑定 在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。通常的形式是这样的: something = new MyClass(..); 在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。 优先级1、四条规则的优先级 new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。 2、判断this 可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断: (1)函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。 var bar = new foo(); (2)函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。 var bar = foo.call(obj2); (3)函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。 var bar = obj1.foo(); (4)如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。 var bar = foo(); 绑定例外在某些场景下this的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。 被忽略的this如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则: function foo() { console.log(this.a); } var a = 2; foo.call(null); // 2 那么什么情况下会传入null呢? 一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用。 间接引用另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。 间接引用最容易在赋值时发生: function foo() { console.log(this.a); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2 赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。根据我们之前说过的,这里会应用默认绑定。 软绑定如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力。 function foo() { console.log('name: ' + this.name); } var obj = { name: 'obj' }, obj2 = { name: 'obj2' }, obj3 = { name: 'obj3' }; var fooOBJ = foo.softBind(obj); fooOBJ(); // name: obj obj2.foo = foo.softBind(obj); obj2.foo(); // name: obj2 <---- 看!! ! fooOBJ.call(obj3); // name: obj3 <---- 看! setTimeout(obj2.foo, 10); // name: obj <---- 应用了软绑定 可以看到,软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。 this词法ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。 箭头函数的词法作用域: function foo() { // 返回一个箭头函数 return a => { //this继承自foo() console.log(this.a); }; } var obj1 = { a: 2, }; var obj2 = { a: 3, }; var bar = foo.call(obj1); bar.call(obj2); // 2, 不是3! foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。 总结我们来总结一下本篇的主要内容:
转自博客园,作者华为云开发联盟https://www.cnblogs.com/huaweiyun/p/18096240 该文章在 2024/3/28 17:20:08 编辑过 |
相关文章
正在查询... |