- 快召唤伙伴们来围观吧
- 微博 QQ QQ空间 贴吧
- 文档嵌入链接
- 复制
- 微信扫一扫分享
- 已成功复制到剪贴板
你不懂JS:this与对象原型
展开查看详情
1 . 目 录 致谢 阅前必读 序 第一章: this 是什么? 为什么要用 this? 困惑 什么是 this? 复习 第二章: this 豁然开朗! 调用点(Call-site) 仅仅是规则 一切皆有顺序 绑定的特例 词法 this 复习 第三章: 对象 语法 类型 内容 迭代 复习 第四章: 混合(淆)“类”的对象 类理论 类机制 类继承 混合(Mixin) 复习 第五章: 原型 Prototype “类” “(原型)继承” 对象链接 复习 本文档使用 书栈(BookStack.CN) 构建 - 1 -
2 . 第六章: 行为委托 迈向面向委托的设计 Classes vs. Objects 更简单的设计 更好的语法 自省 复习 附录A: ES6 class 附录B: 鸣谢 本文档使用 书栈(BookStack.CN) 构建 - 2 -
3 .致谢 致谢 当前文档 《你不懂JS: this 与对象原型(You Dont Know JS)》 由 进击的皇虫 使用 书 栈(BookStack.CN) 进行构建,生成于 2018-02-10。 书栈(BookStack.CN) 仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工 具。 文档内容由网友们编写和整理,书栈(BookStack.CN) 难以确认文档内容知识点是否错漏。如 果您在阅读文档获取知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手, 将知识准确、高效且有效地传递给每一个人。 同时,如果您在日常生活、工作和学习中遇到有价值有营养的知识文档,欢迎分享到 书栈 (BookStack.CN) ,为知识的传承献上您的一份力量! 如果当前文档生成时间太久,请到 书栈(BookStack.CN) 获取最新的文档,以跟上知识更新换 代的步伐。 文档地址:http://www.bookstack.cn/books/You-Dont-Know-JS-this-object- prototypes 书栈官网:http://www.bookstack.cn 书栈开源:https://github.com/TruthHun 分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的 读者,因为我们都将成为知识的传承者。 本文档使用 书栈(BookStack.CN) 构建 - 3 -
4 .阅前必读 阅前必读 你不懂JS: this 与对象原型 从 O’Reilly 购买数字/印刷版 序(Nick Berardi) 第一章: this 还是That? 第二章: this 豁然开朗! 第三章: 对象 第四章: 混合(淆)“类”的对象 第五章: 原型 第六章: 行为委托 附录A: ES6 class 附录B: 鸣谢 本文档使用 书栈(BookStack.CN) 构建 - 4 -
5 .阅前必读 本文档使用 书栈(BookStack.CN) 构建 - 5 -
6 .序 序 序 序 在我读这本书为写这篇序言做准备时,我被迫反思我是如何学习 JavaScript,和在我用它进行编程 和开发的最近15年它改变了多少。 当我15年前开始使用 JavaScript 时,在你的网页上使用 CSS 和 JS 这样的非 HTML 技术的做 法称为 DHTML 或动态 HTML。回到那时,JavaScript 的用途有很大的不同,并且倾向于在你的网 页上加入动画雪花,或者在状态栏上显示告知时间的动态时钟。可以说,在我的职业生涯早期,因为 这些我经常能在因特网上找到的新奇小玩意儿,我真的没有太注意 JavaScript。 直到 2005 年我第一次重新认识到 JavaScript 是一个我需要更加重视的真正的编程语言。在挖 掘研究了 Google Maps 的第一个 beta 版后,我被它的潜力吸引住了。那时,Google Maps 是 第一个同种类的应用 —— 它允许你用鼠标移动地图,放缩,请求服务器而不必刷新页面 —— 都是通 过 JavaScript。它看起来就像魔法! 当什么东西看起来像魔法时,这通常都一个好的信号:你正处在用新方法做事的黎明。噢,我没有错 —— 快进到今天,我敢说 JavaScript 是同时用于客户端和服务器端编程的主要语言之一,而且我 不会用其他方式这么说。 在我回顾过去的15年时,我的一个遗憾是在 2005 年以前我没有给 JavaScript 更多机会,或者 更确切地说,我缺乏远见来看到 JavaScript 是一个真正的编程语言,就像 C++,C#,Java 和许 多其他语言一样有用。 如果我在自己的职业生涯一开始就拥有这套 你不懂JS 系列丛书,我们的职业经历将和今天有很大的 不同。我喜欢这个系列的一个地方是:当你通读这个系列时,它在建立你的理解的水平上,而且用一 种有趣且信息丰富的方式讲解 JS。 this 与对象原型 是这个系列的一个绝妙的续作。它漂亮且自然地建立于前一本书之上,作用域与闭 包,将知识扩展至 JS 语言中十分重要的部分, this 关键字和原型。这两个简单的东西是你将在 未来的书中学到的东西的枢纽,因为他们是用 JavaScript 进行真正的编程的基础。如何创建对 象,关联它们,和扩展它们来表达你的应用中的东西,是用 JavaScript 建立大型和复杂应用程序 所必要的。没有它们,用 JavaScript 制造复杂应用程序(比如 Google Maps)将是不可能的。 我敢说绝大多数 web 开发者可能从没建立过 JavaScript 对象,而只是将这个语言当做按钮和 AJAX 请求的事件绑定胶水。我曾经在我职业生涯的某一点上属于这个群体,但是当我学习了如何掌 握原型和在 JavaScript 中创建对象后,一个充满可能性的世界向我打开了大门。如果你属于仅仅 会写事件绑定胶水代码的那一类,这本书是必读的;如果你只是需要进修,这本书是你一定会用到的 资源。不管怎样,你不会失望的。相信我! 本文档使用 书栈(BookStack.CN) 构建 - 6 -
7 .序 Nick Berardi nickberardi.com, @nberardi 本文档使用 书栈(BookStack.CN) 构建 - 7 -
8 .第一章: this 是什么? 第一章: this 是什么? 第一章: this 是什么? 第一章: this 是什么? JavaScript 中最令人困惑的机制之一就是 this 关键字。它是一个在每个函数作用域中自动定 义的特殊标识符关键字,但即便是一些老练的 JavaScript 开发者也对它到底指向什么感到困扰。 任何足够 先进 的技术都跟魔法没有区别。— Arthur C. Clarke JavaScript 的 this 机制实际上没有 那么 先进,但是开发者们总是在大脑中插入“复 杂”和“混乱”来解释这句话,毫无疑问,如果没有清晰的理解,在 你的 困惑中 this 可能看起来 就是彻头彻尾的魔法。 注意: “this”这个词是在一般的论述中极常用的代词。所以,特别是在口头论述中,很难确定我们 是在将“this”作为一个代词使用,还是在将它作为一个实际的关键字标识符使用。为了表意清晰,我 会总是使用 this 来代表特殊的关键字,而在其他情况下使用“this”或 this 或 this。 为什么要用 this? 困惑 什么是 this? 复习 本文档使用 书栈(BookStack.CN) 构建 - 8 -
9 .为什么要用 this? 为什么要用 this? 为什么要用 this ? 如果对于那些老练的 JavaScript 开发者来说 this 机制都是如此的令人费解,那么有人会问 为什么这种机制会有用?它带来的麻烦不是比好处多吗?在讲解 如何 有用之前,我们应当先来看看 为什么 有用。 让我们试着展示一下 this 的动机和用途: 1. function identify() { 2. return this.name.toUpperCase(); 3. } 4. 5. function speak() { 6. var greeting = "Hello, I'm " + identify.call( this ); 7. console.log( greeting ); 8. } 9. 10. var me = { 11. name: "Kyle" 12. }; 13. 14. var you = { 15. name: "Reader" 16. }; 17. 18. identify.call( me ); // KYLE 19. identify.call( you ); // READER 20. 21. speak.call( me ); // Hello, I'm KYLE 22. speak.call( you ); // Hello, I'm READER 如果这个代码段 如何 工作让你困惑,不要担心!我们很快就会讲解它。只是简要地将这些问题放在 旁边,以便于我们可以更清晰的探究 为什么。 这个代码片段允许 identify() 和 speak() 函数对多个 环境 对象( me 和 you )进行 复用,而不是针对每个对象定义函数的分离版本。 与使用 this 相反地,你可以明确地将环境对象传递给 identify() 和 speak() 。 1. function identify(context) { 2. return context.name.toUpperCase(); 本文档使用 书栈(BookStack.CN) 构建 - 9 -
10 .为什么要用 this? 3. } 4. 5. function speak(context) { 6. var greeting = "Hello, I'm " + identify( context ); 7. console.log( greeting ); 8. } 9. 10. identify( you ); // READER 11. speak( me ); // Hello, I'm KYLE 然而, this 机制提供了更优雅的方式来隐含地“传递”一个对象引用,导致更加干净的API设计和 更容易的复用。 你的使用模式越复杂,你就会越清晰地看到:将执行环境作为一个明确参数传递,通常比传递 this 执行环境要乱。当我们探索对象和原型时,你将会看到一组可以自动引用恰当执行环境对象 的函数是多么有用。 本文档使用 书栈(BookStack.CN) 构建 - 10 -
11 .困惑 困惑 困惑 它自己 它的作用域 困惑 我们很快就要开始讲解 this 是如何 实际 工作的,但我们首先要摒弃一些误解——它实际上 不是 如何工作的。 在开发者们用太过于字面的方式考虑“this”这个名字时就会产生困惑。这通常会产生两种臆测,但都 是不对的。 它自己 第一种常见的倾向是认为 this 指向函数自己。至少,这是一种语法上的合理推测。 为什么你想要在函数内部引用它自己?最常见的理由是递归(在函数内部调用它自己)这样的情形, 或者是一个在第一次被调用时会解除自己绑定的事件处理器。 初次接触 JS 机制的开发者们通常认为,将函数作为一个对象(JavaScript 中所有的函数都是对 象!),可以让你在方法调用之间储存 状态(属性中的值)。这当然是可能的,而且有一些有限的用 处,但这本书的其余部分将会阐述许多其他的模式,提供比函数对象 更好 的地方来存储状态。 过一会儿我们将探索一个模式,来展示 this 是如何不让一个函数像我们可能假设的那样,得到它 自身的引用的。 考虑下面的代码,我们试图追踪函数( foo )被调用了多少次: 1. function foo(num) { 2. console.log( "foo: " + num ); 3. 4. // 追踪 `foo` 被调用了多少次 5. this.count++; 6. } 7. 8. foo.count = 0; 9. 10. var i; 11. 12. for (i=0; i<10; i++) { 13. if (i > 5) { 本文档使用 书栈(BookStack.CN) 构建 - 11 -
12 .困惑 14. foo( i ); 15. } 16. } 17. // foo: 6 18. // foo: 7 19. // foo: 8 20. // foo: 9 21. 22. // `foo` 被调用了多少次? 23. console.log( foo.count ); // 0 -- 这他妈怎么回事……? foo.count 依然 是 0 , 即便四个 console.log 语句明明告诉我们 foo(..) 实际上被调 用了四次。这种挫败来源于对于 this (在 this.count++ 中)的含义进行了 过于字面化 的解 释。 当代码执行 foo.count = 0 时,它确实向函数对象 foo 添加了一个 count 属性。但是对 于函数内部的 this.count 引用, this 其实 根本就不 指向那个函数对象,即便属性名称一 样,但根对象也不同,因而产生了混淆。 注意: 一个负责任的开发者 应当 在这里提出一个问题:“如果我递增的 count 属性不是我以为 的那个,那是哪个 count 被我递增了?”。实际上,如果他再挖的深一些,他会发现自己不小心创 建了一个全局变量 count (第二章解释了这是 如何 发生的!),而且它当前的值是 NaN 。当 然,一旦他发现这个不寻常的结果后,他会有一堆其他的问题:“它怎么是全局的?为什么它是 NaN 而不是某个正确的计数值?”。(见第二章) 与停在这里来深究为什么 this 引用看起来不是如我们 期待 的那样工作,并且回答那些尖锐且 重要的问题相反,许多开发者简单地完全回避这个问题,转向一些其他的另类解决方法,比如创建另 一个对象来持有 count 属性: 1. function foo(num) { 2. console.log( "foo: " + num ); 3. 4. // 追踪 `foo` 被调用了多少次 5. data.count++; 6. } 7. 8. var data = { 9. count: 0 10. }; 11. 12. var i; 13. 14. for (i=0; i<10; i++) { 15. if (i > 5) { 16. foo( i ); 17. } 本文档使用 书栈(BookStack.CN) 构建 - 12 -
13 .困惑 18. } 19. // foo: 6 20. // foo: 7 21. // foo: 8 22. // foo: 9 23. 24. // `foo` 被调用了多少次? 25. console.log( data.count ); // 4 虽然这种方式“解决”了问题是事实,但不幸的是它简单地忽略了真正的问题 —— 缺乏对于 this 的含义和其工作方式上的理解 —— 反而退回到了一个他更加熟悉的机制的舒适区:词法作用域。 注意: 词法作用域是一个完善且有用的机制;我不是在用任何方式贬低它的作用(参见本系列的 “作用域与闭包”)。但在如何使用 this 这个问题上总是靠 猜,而且通常都猜 错,并不是一个 退回到词法作用域,而且从不学习 为什么 this 不跟你合作的好理由。 为了从函数对象内部引用它自己,一般来说通过 this 是不够的。你通常需要通过一个指向它的词 法标识符(变量)得到函数对象的引用。 考虑这两个函数: 1. function foo() { 2. foo.count = 4; // `foo` 引用它自己 3. } 4. 5. setTimeout( function(){ 6. // 匿名函数(没有名字)不能引用它自己 7. }, 10 ); 第一个函数,称为“命名函数”, foo 是一个引用,可以用于在它内部引用自己。 但是在第二个例子中,传递给 setTimeout(..) 的回调函数没有名称标识符(所以被称为“匿名函 数”),所以没有合适的办法引用函数对象自己。 注意: 在函数中有一个老牌儿但是现在被废弃的,而且令人皱眉头的 arguments.callee 引用 也 指向当前正在执行的函数的函数对象。这个引用通常是匿名函数在自己内部访问函数对象的唯一方 法。然而,最佳的办法是完全避免使用匿名函数,至少是对于那些需要自引用的函数,而使用命名函 数(表达式)。 arguments.callee 已经被废弃而且不应该再使用。 对于当前我们的例子来说,另一个 好用的 解决方案是在每一个地方都使用 foo 标识符作为函数 对象的引用,而根本不用 this : 1. function foo(num) { 2. console.log( "foo: " + num ); 3. 本文档使用 书栈(BookStack.CN) 构建 - 13 -
14 .困惑 4. // 追踪 `foo` 被调用了多少次 5. foo.count++; 6. } 7. 8. foo.count = 0; 9. 10. var i; 11. 12. for (i=0; i<10; i++) { 13. if (i > 5) { 14. foo( i ); 15. } 16. } 17. // foo: 6 18. // foo: 7 19. // foo: 8 20. // foo: 9 21. 22. // `foo` 被调用了多少次? 23. console.log( foo.count ); // 4 然而,这种方法也类似地回避了对 this 的 真正 理解,而且完全依靠变量 foo 的词法作用 域。 另一种解决这个问题的方法是强迫 this 指向 foo 函数对象: 1. function foo(num) { 2. console.log( "foo: " + num ); 3. 4. // 追踪 `foo` 被调用了多少次 5. // 注意:由于 `foo` 的被调用方式(见下方),`this` 现在确实是 `foo` 6. this.count++; 7. } 8. 9. foo.count = 0; 10. 11. var i; 12. 13. for (i=0; i<10; i++) { 14. if (i > 5) { 15. // 使用 `call(..)`,我们可以保证 `this` 指向函数对象(`foo`) 16. foo.call( foo, i ); 17. } 18. } 19. // foo: 6 20. // foo: 7 21. // foo: 8 本文档使用 书栈(BookStack.CN) 构建 - 14 -
15 .困惑 22. // foo: 9 23. 24. // `foo` 被调用了多少次? 25. console.log( foo.count ); // 4 与回避 this 相反,我们接受它。 我们马上将会更完整地讲解这样的技术 如何 工作,所以如果 你依然有点儿糊涂,不要担心! 它的作用域 对 this 的含义第二常见的误解,是它不知怎的指向了函数的作用域。这是一个刁钻的问题,因为 在某一种意义上它有正确的部分,而在另外一种意义上,它是严重的误导。 明确地说, this 不会以任何方式指向函数的 词法作用域。作用域好像是一个将所有可用标识符作 为属性的对象,这从内部来说是对的。但是 JavasScript 代码不能访问作用域“对象”。它是 引擎 的内部实现。 考虑下面代码,它(失败的)企图跨越这个边界,用 this 来隐含地引用函数的词法作用域: 1. function foo() { 2. var a = 2; 3. this.bar(); 4. } 5. 6. function bar() { 7. console.log( this.a ); 8. } 9. 10. foo(); //undefined 这个代码段里不只有一个错误。虽然它看起来是在故意瞎搞,但你看到的这段代码,提取自在公共社 区的帮助论坛中被交换的真实代码。真是难以想象对 this 的臆想是多么的误导人。 首先,试图通过 this.bar() 来引用 bar() 函数。它几乎可以说是 碰巧 能够工作,我们过一 会儿再解释它是 如何 工作的。调用 bar() 最自然的方式是省略开头的 this. ,而仅使用标 识符进行词法引用。 然而,写下这段代码的开发者试图用 this 在 foo() 和 bar() 的词法作用域间建立一座 桥,使得 bar() 可以访问 foo() 内部作用域的变量 a 。这样的桥是不可能的。 你不能使用 this 引用在词法作用域中查找东西。这是不可能的。 每当你感觉自己正在试图使用 this 来进行词法作用域的查询时,提醒你自己:这里没有桥。 本文档使用 书栈(BookStack.CN) 构建 - 15 -
16 .困惑 本文档使用 书栈(BookStack.CN) 构建 - 16 -
17 .什么是 this? 什么是 this? 什么是 this ? 我们已经列举了各种不正确的臆想,现在让我们把注意力转移到 this 机制是如何真正工作的。 我们早先说过, this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条 件。 this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。 当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用栈 —— call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性 之一,就是在函数执行期间将被使用的 this 引用。 下一章中,我们将会学习寻找函数的 调用点(call-site) 来判定它的执行如何绑定 this 。 本文档使用 书栈(BookStack.CN) 构建 - 17 -
18 .复习 复习 复习 对于那些没有花时间学习 this 绑定机制如何工作的 JavaScript 开发者来说, this 绑定 一直是困惑的根源。对于 this 这么重要的机制来说,猜测、试错、或者盲目地从 Stack Overflow 的回答中复制粘贴,都不是有效或正确利用它的方法。 为了学习 this ,你必须首先学习 this 不是 什么,不论是哪种把你误导至何处的臆测或误 解。 this 既不是函数自身的引用,也不是函数 词法 作用域的引用。 本文档使用 书栈(BookStack.CN) 构建 - 18 -
19 .第二章: this 豁然开朗! 第二章: this 豁然开朗! 第二章: this 豁然开朗! 链接 第二章: this 豁然开朗! 在第一章中,我们摒弃了种种对 this 的误解,并且知道了 this 是一个完全根据调用点(函 数是如何被调用的)而为每次函数调用建立的绑定。 链接 调用点(Call-site) 仅仅是规则 一切皆有顺序 绑定的特例 词法 this 复习 本文档使用 书栈(BookStack.CN) 构建 - 19 -
20 .调用点(Call-site) 调用点(Call-site) 调用点(Call-site) 为了理解 this 绑定,我们不得不理解调用点:函数在代码中被调用的位置(不是被声明的位 置)。我们必须考察调用点来回答这个问题:这个 this 指向什么? 一般来说寻找调用点就是:“找到一个函数是在哪里被调用的”,但它不总是那么简单,比如某些特定 的编码模式会使 真正的 调用点变得不那么明确。 考虑 调用栈(call-stack) (使我们到达当前执行位置而被调用的所有方法的堆栈)是十分重要 的。我们关心的调用点就位于当前执行中的函数 之前 的调用。 我们来展示一下调用栈和调用点: 1. function baz() { 2. // 调用栈是: `baz` 3. // 我们的调用点是 global scope(全局作用域) 4. 5. console.log( "baz" ); 6. bar(); // <-- `bar` 的调用点 7. } 8. 9. function bar() { 10. // 调用栈是: `baz` -> `bar` 11. // 我们的调用点位于 `baz` 12. 13. console.log( "bar" ); 14. foo(); // <-- `foo` 的 call-site 15. } 16. 17. function foo() { 18. // 调用栈是: `baz` -> `bar` -> `foo` 19. // 我们的调用点位于 `bar` 20. 21. console.log( "foo" ); 22. } 23. 24. baz(); // <-- `baz` 的调用点 在分析代码来寻找(从调用栈中)真正的调用点时要小心,因为它是影响 this 绑定的唯一因素。 注意: 你可以通过按顺序观察函数的调用链在你的大脑中建立调用栈的视图,就像我们在上面代码段 本文档使用 书栈(BookStack.CN) 构建 - 20 -
21 .调用点(Call-site) 中的注释那样。但是这很痛苦而且易错。另一种观察调用栈的方式是使用你的浏览器的调试工具。大 多数现代的桌面浏览器都内建开发者工具,其中就包含 JS 调试器。在上面的代码段中,你可以在调 试工具中为 foo() 函数的第一行设置一个断点,或者简单的在这第一行上插入一个 debugger 语句。当你运行这个网页时,调试工具将会停止在这个位置,并且向你展示一个到达这一行之前所有 被调用过的函数的列表,这就是你的调用栈。所以,如果你想调查 this 绑定,可以使用开发者工 具取得调用栈,之后从上向下找到第二个记录,那就是你真正的调用点。 本文档使用 书栈(BookStack.CN) 构建 - 21 -
22 .仅仅是规则 仅仅是规则 仅仅是规则 默认绑定(Default Binding) 隐含绑定(Implicit Binding) 隐含丢失(Implicitly Lost) 明确绑定(Explicit Binding) 硬绑定(Hard Binding) API 调用的“环境” new 绑定( new Binding) 仅仅是规则 现在我们将注意力转移到调用点 如何 决定在函数执行期间 this 指向哪里。 你必须考察调用点并判定4种规则中的哪一种适用。我们将首先独立地解释一下这4种规则中的每一 种,之后我们来展示一下如果有多种规则可以适用于调用点时,它们的优先顺序。 默认绑定(Default Binding) 我们要考察的第一种规则源于函数调用的最常见的情况:独立函数调用。可以认为这种 this 规则 是在没有其他规则适用时的默认规则。 考虑这个代码段: 1. function foo() { 2. console.log( this.a ); 3. } 4. 5. var a = 2; 6. 7. foo(); // 2 第一点要注意的,如果你还没有察觉到,是在全局作用域中的声明变量,也就是 var a = 2 ,是全局 对象的同名属性的同义词。它们不是互相拷贝对方,它们 就是 彼此。正如一个硬币的两面。 第二,我们看到当 foo() 被调用时, this.a 解析为我们的全局变量 a 。为什么?因为在这种情 况下,对此方法调用的 this 实施了 默认绑定,所以使 this 指向了全局对象。 我们怎么知道这里适用 默认绑定 ?我们考察调用点来看看 foo() 是如何被调用的。在我们的代 码段中, foo() 是被一个直白的,毫无修饰的函数引用调用的。没有其他的我们将要展示的规则适 用于这里,所以 默认绑定 在这里适用。 本文档使用 书栈(BookStack.CN) 构建 - 22 -
23 .仅仅是规则 如果 strict mode 在这里生效,那么对于 默认绑定 来说全局对象是不合法的,所以 this 将 被设置为 undefined 。 1. function foo() { 2. "use strict"; 3. 4. console.log( this.a ); 5. } 6. 7. var a = 2; 8. 9. foo(); // TypeError: `this` is `undefined` 一个微妙但是重要的细节是:即便所有的 this 绑定规则都是完全基于调用点的,但如果 foo() 的 内容 没有在 strict mode 下执行,对于 默认绑定 来说全局对象是 唯一 合法 的; foo() 的调用点的 strict mode 状态与此无关。 1. function foo() { 2. console.log( this.a ); 3. } 4. 5. var a = 2; 6. 7. (function(){ 8. "use strict"; 9. 10. foo(); // 2 11. })(); 注意: 在你的代码中故意混用 strict mode 和非 strict mode 通常是让人皱眉头的。你的程 序整体可能应当不是 Strict 就是 非 Strict。然而,有时你可能会引用与你的 Strict 模式不 同的第三方包,所以对这些微妙的兼容性细节要多加小心。 隐含绑定(Implicit Binding) 另一种要考虑的规则是:调用点是否有一个环境对象(context object),也称为拥有者 (owning)或容器(containing)对象,虽然这些名词可能有些误导人。 考虑这段代码: 1. function foo() { 2. console.log( this.a ); 3. } 4. 本文档使用 书栈(BookStack.CN) 构建 - 23 -
24 .仅仅是规则 5. var obj = { 6. a: 2, 7. foo: foo 8. }; 9. 10. obj.foo(); // 2 首先,注意 foo() 被声明然后作为引用属性添加到 obj 上的方式。无论 foo() 是否一开 始就在 obj 上被声明,还是后来作为引用添加(如上面代码所示),这个 函数 都不被 obj 所真正“拥有”或“包含”。 然而,调用点 使用 obj 环境来 引用 函数,所以你 可以说 obj 对象在函数被调用的时间 点上“拥有”或“包含”这个 函数引用。 不论你怎样称呼这个模式,在 foo() 被调用的位置上,它被冠以一个指向 obj 的对象引用。 当一个方法引用存在一个环境对象时,隐含绑定 规则会说:是这个对象应当被用于这个函数调用的 this 绑定。 因为 obj 是 foo() 调用的 this ,所以 this.a 就是 obj.a 的同义词。 只有对象属性引用链的最后一层是影响调用点的。比如: 1. function foo() { 2. console.log( this.a ); 3. } 4. 5. var obj2 = { 6. a: 42, 7. foo: foo 8. }; 9. 10. var obj1 = { 11. a: 2, 12. obj2: obj2 13. }; 14. 15. obj1.obj2.foo(); // 42 隐含丢失(Implicitly Lost) this 绑定最常让人沮丧的事情之一,就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会 退回到 默认绑定, 根据 strict mode 的状态,其结果不是全局对象就是 undefined 。 考虑这段代码: 本文档使用 书栈(BookStack.CN) 构建 - 24 -
25 .仅仅是规则 1. function foo() { 2. console.log( this.a ); 3. } 4. 5. var obj = { 6. a: 2, 7. foo: foo 8. }; 9. 10. var bar = obj.foo; // 函数引用! 11. 12. var a = "oops, global"; // `a` 也是一个全局对象的属性 13. 14. bar(); // "oops, global" 尽管 bar 似乎是 obj.foo 的引用,但实际上它只是另一个 foo 本身的引用而已。另外, 起作用的调用点是 bar() ,一个直白,毫无修饰的调用,因此 默认绑定 适用于这里。 这种情况发生的更加微妙,更常见,而且更意外的方式,是当我们考虑传递一个回调函数时: 1. function foo() { 2. console.log( this.a ); 3. } 4. 5. function doFoo(fn) { 6. // `fn` 只不过 `foo` 的另一个引用 7. 8. fn(); // <-- 调用点! 9. } 10. 11. var obj = { 12. a: 2, 13. foo: foo 14. }; 15. 16. var a = "oops, global"; // `a` 也是一个全局对象的属性 17. 18. doFoo( obj.foo ); // "oops, global" 参数传递仅仅是一种隐含的赋值,而且因为我们在传递一个函数,它是一个隐含的引用赋值,所以最 终结果和我们前一个代码段一样。 那么如果接收你所传递回调的函数不是你的,而是语言内建的呢?没有区别,同样的结果。 1. function foo() { 2. console.log( this.a ); 本文档使用 书栈(BookStack.CN) 构建 - 25 -
26 .仅仅是规则 3. } 4. 5. var obj = { 6. a: 2, 7. foo: foo 8. }; 9. 10. var a = "oops, global"; // `a` 也是一个全局对象的属性 11. 12. setTimeout( obj.foo, 100 ); // "oops, global" 把这个粗糙的,理论上的 setTimeout() 假想实现当做 JavaScript 环境内建的实现的话: 1. function setTimeout(fn,delay) { 2. // (通过某种方法)等待 `delay` 毫秒 3. fn(); // <-- 调用点! 4. } 正如我们刚刚看到的,我们的回调函数丢掉他们的 this 绑定是十分常见的事情。但是 this 使我们吃惊的另一种方式是,接收我们回调的函数故意改变调用的 this 。那些很流行的 JavaScript 库中的事件处理器就十分喜欢强制你的回调的 this 指向触发事件的 DOM 元素。 虽然有时这很有用,但其他时候这简直能气死人。不幸的是,这些工具很少给你选择。 不管哪一种意外改变 this 的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你 (还)没有办法控制调用点给你一个故意的绑定。我们很快就会看到一个方法,通过 固定 this 来解决这个问题。 明确绑定(Explicit Binding) 用我们刚看到的 隐含绑定,我们不得不改变目标对象使它自身包含一个对函数的引用,而后使用这个 函数引用属性来间接地(隐含地)将 this 绑定到这个对象上。 但是,如果你想强制一个函数调用使用某个特定对象作为 this 绑定,而不在这个对象上放置一个 函数引用属性呢? JavaScript 语言中的“所有”函数都有一些工具(通过他们的 [[Prototype]] —— 待会儿详述) 可以用于这个任务。具体地说,函数拥有 call(..) 和 apply(..) 方法。从技术上讲, JavaScript 宿主环境有时会提供一些(说得好听点儿!)很特别的函数,它们没有这些功能。但这 很少见。绝大多数被提供的函数,当然还有你将创建的所有的函数,都可以访问 call(..) 和 apply(..) 。 这些工具如何工作?它们接收的第一个参数都是一个用于 this 的对象,之后使用这个指定的 this 来调用函数。因为你已经直接指明你想让 this 是什么,所以我们称这种方式为 明确绑 定(explicit binding)。 本文档使用 书栈(BookStack.CN) 构建 - 26 -
27 .仅仅是规则 考虑这段代码: 1. function foo() { 2. console.log( this.a ); 3. } 4. 5. var obj = { 6. a: 2 7. }; 8. 9. foo.call( obj ); // 2 通过 foo.call(..) 使用 明确绑定 来调用 foo ,允许我们强制函数的 this 指向 obj 。 如果你传递一个简单基本类型值( string , boolean ,或 number 类型)作为 this 绑 定,那么这个基本类型值会被包装在它的对象类型中(分别是 new String(..) , new Boolean(..) ,或 new Number(..) )。这通常称为“封箱(boxing)”。 注意: 就 this 绑定的角度讲, call(..) 和 apply(..) 是完全一样的。它们确实在处理 其他参数上的方式不同,但那不是我们当前关心的。 不幸的是,单独依靠 明确绑定 仍然不能为我们先前提到的问题提供解决方案,也就是函数“丢失”自 己原本的 this 绑定,或者被第三方框架覆盖,等等问题。 硬绑定(Hard Binding) 但是有一个 明确绑定 的变种确实可以实现这个技巧。考虑这段代码: 1. function foo() { 2. console.log( this.a ); 3. } 4. 5. var obj = { 6. a: 2 7. }; 8. 9. var bar = function() { 10. foo.call( obj ); 11. }; 12. 13. bar(); // 2 14. setTimeout( bar, 100 ); // 2 15. 16. // `bar` 将 `foo` 的 `this` 硬绑定到 `obj` 17. // 所以它不可以被覆盖 本文档使用 书栈(BookStack.CN) 构建 - 27 -
28 .仅仅是规则 18. bar.call( window ); // 2 我们来看看这个变种是如何工作的。我们创建了一个函数 bar() ,在它的内部手动调用 foo.call(obj) ,由此强制 this 绑定到 obj 并调用 foo 。无论你过后怎样调用函数 bar ,它总是手动使用 obj 调用 foo 。这种绑定即明确又坚定,所以我们称之为 硬绑定 (hard binding) 用 硬绑定 将一个函数包装起来的最典型的方法,是为所有传入的参数和传出的返回值创建一个通 道: 1. function foo(something) { 2. console.log( this.a, something ); 3. return this.a + something; 4. } 5. 6. var obj = { 7. a: 2 8. }; 9. 10. var bar = function() { 11. return foo.apply( obj, arguments ); 12. }; 13. 14. var b = bar( 3 ); // 2 3 15. console.log( b ); // 5 另一种表达这种模式的方法是创建一个可复用的帮助函数: 1. function foo(something) { 2. console.log( this.a, something ); 3. return this.a + something; 4. } 5. 6. // 简单的 `bind` 帮助函数 7. function bind(fn, obj) { 8. return function() { 9. return fn.apply( obj, arguments ); 10. }; 11. } 12. 13. var obj = { 14. a: 2 15. }; 16. 17. var bar = bind( foo, obj ); 18. 本文档使用 书栈(BookStack.CN) 构建 - 28 -
29 .仅仅是规则 19. var b = bar( 3 ); // 2 3 20. console.log( b ); // 5 由于 硬绑定 是一个如此常用的模式,它已作为 ES5 的内建工具提 供: Function.prototype.bind ,像这样使用: 1. function foo(something) { 2. console.log( this.a, something ); 3. return this.a + something; 4. } 5. 6. var obj = { 7. a: 2 8. }; 9. 10. var bar = foo.bind( obj ); 11. 12. var b = bar( 3 ); // 2 3 13. console.log( b ); // 5 bind(..) 返回一个硬编码的新函数,它使用你指定的 this 环境来调用原本的函数。 注意: 在 ES6 中, bind(..) 生成的硬绑定函数有一个名为 .name 的属性,它源自于原始的 目标函数(target function)。举例来说: bar = foo.bind(..) 应该会有一个 bar.name 属 性,它的值为 "bound foo" ,这个值应当会显示在调用栈轨迹的函数调用名称中。 API 调用的“环境” 确实,许多库中的函数,和许多在 JavaScript 语言以及宿主环境中的内建函数,都提供一个可选 参数,通常称为“环境(context)”,这种设计作为一种替代方案来确保你的回调函数使用特定的 this 而不必非得使用 bind(..) 。 举例来说: 1. function foo(el) { 2. console.log( el, this.id ); 3. } 4. 5. var obj = { 6. id: "awesome" 7. }; 8. 9. // 使用 `obj` 作为 `this` 来调用 `foo(..)` 10. [1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome 本文档使用 书栈(BookStack.CN) 构建 - 29 -