You Don‘t Know JS 系列(俄语仓库)之《Types Grammar》附录 A:混合环境中的 JavaScript 实战指南
教程文档【免费下载链接】you-dont-know-js-ru Russian translation of You Dont Know JS book series项目地址https://gitcode.com/gh_mirrors/yo/you-dont-know-js-ru点击查看免费下载导读本篇文章基于开源仓库 you-dont-know-js-ru 中《Types Grammar》卷的附录 AapA.md展开。该书卷正文已经系统讲解了 JS 语言核心类型、值、原生对象、强制转换、语法而本附录聚焦于语言核心之外的现实世界当你的 JS 代码运行在浏览器、Node.js 等宿主环境中、与第三方代码共存时会遭遇哪些规范偏差与不确定性。读完本文你将掌握 Annex B / Web ECMAScript 的浏览器兼容性差异、宿主对象Host Objects的特殊行为、全局 DOM 变量陷阱、原生原型扩展的风险与 Polyfill 的正确姿势、多个script之间的程序边界、保留字规则以及不同引擎的实现限制从而写出更健壮、更可预测的前端代码。一、背景为什么语言规范之外还有混合环境本书正文探讨的核心语言机制在纯引擎环境下执行时行为完全由规范决定、高度可预测。但现实中的 JS 几乎总是运行在某个**宿主环境hosting environment**之中——浏览器、Node.js、Rhino 等——你的代码还会与其他来源的代码一起运行甚至可能跑在非浏览器的 JS 引擎上。此时一些行为可能与纯规范预期不同。这正是本附录存在的意义语言本身只有一份标准ECMAScript但运行时环境会引入偏差。本章剩余内容将逐类剖析这些偏差。二、Annex BECMAScript官方规范里的浏览器偏差一个鲜为人知的事实是语言的官方名称是ECMAScript由 ECMA 标准组织管理而JavaScript 只是该语言的通用商标名更准确地说JavaScript 是规范在浏览器中的实现。官方 ECMAScript 规范包含一个 Annex B 章节专门讨论为了浏览器中的 JS 兼容性而对官方规范做出的特定偏差。理解这些偏差的正确姿势是它们只有在你的代码运行于浏览器时才可靠地存在/有效。如果你的代码只跑在浏览器里你不会看到任何可观察到的差异但如果代码可能运行在 Node.js、Rhino 等非浏览器环境或你不确定运行环境就要小心谨慎。2.1 Annex B 的主要兼容性差异原文档列举了三个主要差异点八进制数字字面量在非strict mode下允许形如0123十进制83的八进制字面量。window.escape(..)与window.unescape(..)允许用%分隔的十六进制转义序列来转义/反转义字符串。例如window.escape( ?foo97%bar3% ); // 结果: %3Ffoo%3D97%25%26bar%3D3%25String.prototype.substr与String.prototype.substring非常相似区别在于第二个参数不是结束索引不含而是长度要包含的字符个数。这些差异在日常代码中极少被使用因此规范偏差通常不是大问题——但如果你依赖了其中任何一项就要格外小心。2.2 Web ECMAScript浏览器必须做但 Annex B 尚未收录的差异Web ECMAScript 规范http://javascript.spec.whatwg.org/覆盖了官方 ECMAScript 规范与浏览器当前 JS 实现之间的差异。换句话说以下条目是浏览器为了彼此兼容而被要求做到的但在写作时尚未被列入官方规范的 Annex B!--和--是合法的单行注释分隔符详见下文script标签一节的历史缘由。String.prototype的 HTML 格式化方法anchor(..)、big(..)、blink(..)、bold(..)、fixed(..)、fontcolor(..)、fontsize(..)、italics(..)、link(..)、small(..)、strike(..)、sub(..)。注意这些方法在实践中极少使用通常被弃用官方建议改用内置 DOM API 或自定义工具函数。RegExp扩展RegExp.$1..RegExp.$9匹配分组以及RegExp.lastMatch/RegExp[$]最近一次匹配。Function.prototype扩展Function.prototype.arguments别名内部arguments对象和Function.caller别名内部arguments.caller。注意arguments以及arguments.caller已被弃用应尽量避免使用这些别名更是双重不建议——不要用它们。说明还有一些其他次要且极少使用的偏差未列入清单如需更详细信息请查阅外部的 Annex B 与 Web ECMAScript 文档。总体上以上差异都很少用到规范偏差不是显著问题——只要你不依赖它们。三、宿主对象Host Objects环境给你的特殊对象本书前面章节详细覆盖了变量在 JS 中的行为规则但这些规则对由环境自动定义、或由宿主环境浏览器等创建并提供给 JS的变量存在例外——这些变量被称为宿主对象host objects既包含内置object也包含内置function。例如var a document.createElement( div ); typeof a; // object -- 符合预期 Object.prototype.toString.call( a ); // [object HTMLDivElement] a.tagName; // DIV这里的a不只是普通object而是一个特殊的宿主对象——因为它是 DOM 元素。它拥有不同的内部[[Class]]值HTMLDivElement并带有预定义且常常不可更改的属性。关于内部[[Class]]的机制可回顾本书第 3 章Natives中内部属性[[Class]]一节——普通对象通过借用Object.prototype.toString()可以揭示其内部[[Class]]标签如Array、RegExp而宿主对象则带有如HTMLDivElement这样的环境专属标签。另一个已在第 4 章Coercion的 Falsy Objects 一节中详述的怪癖是有些对象存在但被强制转换为boolean时会令人困惑地变成false而非预期的true。最著名的例子是document.all——一个由 DOM而非 JS 引擎本身提供给 JS 程序的数组状对象它曾经被用作旧版 IE 检测手段如今出于历史兼容性而被设计成假对象。宿主对象可能还有其他行为变异需要注意无法访问普通object的内置方法如toString()不可被覆盖带有某些预定义的只读属性其方法无法通过this绑定到其他对象等等……宿主对象对 JS 代码与周边环境的协作至关重要但当你与宿主对象交互时务必意识到它的特殊性不要想当然地假设其行为与普通 JSobject一致。一个你几乎每天都在交互的宿主对象典型例子是console对象及其各种方法log(..)、error(..)等。console由宿主环境提供专门用于代码输出各种开发相关信息在浏览器中它连接到开发者工具的 console 面板而在 Node.js 及其他服务端 JS 环境中console通常连接到 JS 环境系统进程的标准输出stdout和标准错误stderr流。四、全局 DOM 变量HTML 页面也会创建全局变量你可能已经知道在全局作用域声明一个变量无论是否用var不仅会创建一个全局变量还会在全局对象浏览器中是window上创建同名属性。但鲜为人知的是源于遗留浏览器行为为 DOM 元素设置id属性也会创建同名的全局变量。例如HTML 中div idfoo/div配合 JSif (typeof foo undefined) { foo 42; // 永远不会执行 } console.log( foo ); // HTML 元素你也许习惯于用typeof或.. in window检查来管理全局变量探测并假设只有 JS 代码才会创建这些变量。但如上所示宿主 HTML 页面的内容同样能创建全局变量——如果不小心这会轻易破坏你的存在性检查逻辑。这是尽量少用全局变量的又一个理由如果必须使用请使用极不可能冲突的唯一名称并且不仅要避免与其它代码冲突还要避免与 HTML 内容冲突。五、原生原型Native Prototypes永远不要扩展原生原型JS 社区最经典的最佳实践智慧之一是永远不要扩展原生原型。原因在于无论你想给Array.prototype添加什么当前尚不存在的方法或属性名只要它是有用的、设计良好的、命名恰当的就很有可能会最终被加入规范——到那时你的扩展就会与新规范冲突。5.1 一个真实的教训Array.prototype.push冲突原文档作者分享了一个亲身经历他在为一个网站构建可嵌入 widget依赖 jQuery任何框架都躲不开这个坑几乎所有站点都能正常工作唯独有一个站点完全崩溃。经过近一周的分析调试他发现该站点某个遗留文件中埋藏着这样的代码// Netscape 4 没有 Array.push Array.prototype.push function(item) { this[this.length] item; };抛开那条疯狂的注释如今谁还关心 Netscape 4这段代码看起来合理对吧问题在于Array.prototype.push后来被加入规范时其语义与这段代码不兼容。标准push(..)允许一次压入多个元素而这个 hack 版本会忽略后续元素。几乎所有 JS 框架的代码都依赖push(..)一次传入多个元素——在该案例中被彻底搞坏的是 CSS 选择器引擎周边的代码而可能存在问题的位置远不止几十处。写这个 hack 的开发者本意正确命名为push但没预见到压入多个元素的需求他是善意的却埋下了一颗直到近十年后才被引爆的地雷。5.2 教训一不要无条件扩展原生首先除非你能 100% 确定自己的代码是环境中唯一运行的代码否则不要扩展原生。做不到 100% 确定扩展原生就是危险的必须权衡风险。其次不要无条件地定义扩展因为可能意外覆盖原生。如果那段代码写成if (!Array.prototype.push) { // Netscape 4 没有 Array.push Array.prototype.push function(item) { this[this.length] item; }; }if守卫会让这个 hack 版push()只在它不存在的 JS 环境中定义。但即使这种写法也有风险如果站点代码出于某种疯狂的原因依赖一个忽略多元素的push(..)那么早在标准push(..)推出时该代码就已经坏了如果任何其他库先于这个if守卫用一个不兼容的实现 hack 了push(..)站点当时就会被破坏。5.3 更深层的问题能否信任原生内置行为这个案例引出一个鲜少被 JS 开发者关注的深刻问题当你的代码运行在任何不是唯一代码的环境中时你到底应不应该依赖原生内置行为严格答案是不应该但这极不现实——你的代码通常无法为自己重新定义一份私有的、不可触碰的内置行为全集即使能也太过浪费。那么是否应该对内置行为做特性检测并做一致性compliance测试来确认它符合预期如果测试失败代码是否应该拒绝运行// 不要信任 Array.prototype.push (function(){ if (Array.prototype.push) { var a []; a.push(1,2); if (a[0] 1 a[1] 2) { // 测试通过可以安全使用 return; } } throw Error( Array#push() is missing/broken! ); })();理论上说得通但要为每一个内置方法设计测试同样不切实际。那么到底该怎么办是信任但验证feature compliance test一切还是假设存在即合规让他人造成的破坏自行暴露没有完美的答案。唯一可观察到的事实是扩展原生原型是这些问题咬到你的唯一途径——如果你不扩展应用代码中也没人扩展你就安全否则就应该建立至少一点怀疑、悲观和对潜在破坏的预期。拥有一套在你的所有已知环境中运行的完整单元/回归测试可以在早期暴露部分问题但并不能真正保护你免受这些冲突的伤害。5.4 Shims / Polyfills唯一相对安全的扩展位置通常认为扩展原生的唯一安全位置是旧的不合规范的环境——因为旧环境不太可能改变新浏览器以新规范特性替换旧浏览器的方式演进而不是去修补旧浏览器。如果你能预知未来、确切知道某个未来标准比如Array.prototype.foobar长什么样那么现在就写一个兼容版本使用是绝对安全的对吧if (!Array.prototype.foobar) { // silly, silly Array.prototype.foobar function() { this.push( foo, bar ); }; }如果Array.prototype.foobar已有规范、且规范行为与这段逻辑一致那么定义这样的代码段就相当安全此时它一般被称为polyfill或 shim。这类代码对修补未更新到最新规范的旧浏览器环境非常有用。使用 polyfill 是在所有受支持环境中获得可预测代码的好方法。扩展知识ES5-Shim / ES6-Shim / 转译器分工ES5-Shim 是一个全面的 shims/polyfills 集合把项目提升到 ES5 基线ES6-Shim 为 ES6 新增的 API 提供 shims注意API 可以 shim/polyfill新语法一般不行。要弥合语法鸿沟你还需要 ES6-to-ES5 转译器如 Traceur。Prolyfill可能-fill如果某个标准即将到来且大多数讨论已就它叫什么、如何运作达成一致那么提前为其创建面向未来合规的 polyfill 就叫prollyfill。真正的难题在于某些新标准行为无法完全被 polyfill/prollyfill。社区中存在争论对常见场景提供部分 polyfill 是否可接受并记录无法 polyfill 的部分还是说若无法 100% 符合规范就该完全避免 polyfill。许多开发者至少接受一些常见的部分 polyfill比如Object.create(..)因为他们不会使用未被覆盖的部分。还有些开发者认为 polyfill/shim 的if守卫应包含某种一致性测试要么方法缺失、要么测试失败时才替换现有方法。这层额外的一致性检查有时被用来区分shim做过一致性测试与polyfill仅做存在性检查。唯一绝对的结论是这里没有绝对正确的答案。扩展原生即使在旧环境中安全地做也不是 100% 安全在存在他人代码时依赖可能被扩展的原生同样如此。无论哪种方式都应带着谨慎、防御性代码以及大量关于风险的显式文档去执行。六、script标签多个文件是独立程序还是同一程序大多数浏览器网站/应用都有不止一个代码文件页面上常见几个甚至十几个script src../script元素分别加载这些文件外加一些内联script .. /script元素。那么问题来了这些独立的文件/代码片段是彼此独立的程序还是共同构成一个 JS 程序也许令人惊讶的现实是在大多数方面但不是所有方面它们表现得更像独立的 JS 程序。6.1 它们共享什么它们唯一共享的是单一的global对象浏览器中是window这意味着多个文件可以把各自的代码附加到这一共享命名空间中并相互交互。所以如果一个script元素定义了全局函数foo()当第二个script稍后运行时它可以像自己定义的一样访问并调用foo()。6.2 它们不共享什么跨边界的提升但全局变量作用域提升hoisting不会跨越这些边界参见本系列的《Scope Closures》卷。因此以下代码不工作因为foo()的声明尚未发生无论它们是内联script元素还是外部script src../script文件scriptfoo();/script script function foo() { .. } /script但以下两种写法都可行script foo(); function foo() { .. } /script或者script function foo() { .. } /script scriptfoo();/script6.3 错误隔离与动态创建此外如果某个script元素内联或外部发生错误作为独立 JS 程序它会失败并停止但后续的script仍会共享同一个global不受阻碍地继续运行。你还可以从代码中动态创建script元素并注入页面 DOM其行为基本等同于正常加载的独立文件var greeting Hello World; var el document.createElement( script ); el.text function foo(){ alert( greeting );\ } setTimeout( foo, 1000 );; document.body.appendChild( el );注意如果上述代码不设置el.text为代码内容、而是设置el.src指向某个文件 URL那你就动态创建了一个外部加载的script src../script元素。6.4 内联代码的坑/script终结符内联代码块与外部文件代码的一个区别是在内联代码块中/script字符序列不能连续出现——无论出现在哪里它都会被解释为代码块的结束。所以要小心类似代码script var code scriptalert( Hello World )/script; /script它看起来无害但字符串字面量中的/script会异常终止脚本块导致错误。最常见的规避写法是/sc ript;6.5 字符集与过时的注释包裹还要注意外部文件中的代码会按文件被提供或默认的字符集UTF-8、ISO-8859-8 等解释而 HTML 页面内联script元素中的代码会按页面或默认的字符集解释。警告charset属性对内联 script 元素无效。另一个已废弃的做法是在内联script元素中用 HTML 风格或 X(HT)ML 风格的注释包裹代码script !-- alert( Hello ); //-- /script script !--//--![CDATA[//!-- alert( World ); //--!]] /script如今这两种做法都完全没必要——如果你还在这样做停下来附注!--和--HTML 风格注释之所以在 JavaScript 中也被规范为合法的单行注释分隔符var x 2; !-- valid comment和-- another valid line comment见上文 Web ECMAScript 一节正是因为这个旧技术的存在。但永远不要使用它们。七、保留字Reserved Words哪些名字你不能用ES5 规范第 7.6.1 节定义了一组不能用作独立变量名的保留字。技术上讲有四类关键字keywords、未来保留字future reserved words、null字面量、以及true/false布尔字面量。关键字是那些显而易见的如function、switch。未来保留字包括enum等——不过其中很多class、extends等如今已被 ES6 实际使用还有一些仅限严格模式的保留字如interface。StackOverflow 用户 art4theSould 创造性地把所有这些保留字写成了一首有趣的小诗原文略见原文档Let this long package float, Goto private class if short. While protected with debugger case, Continue volatile interface. Instanceof super synchronized throw, Extends final export throws. Try import double enum?False, boolean, abstract function, Implements typeof transient break! Void static, default do, Switch int native new. Else, delete null public var In return for const, true, char …Finally catch byte.注这首诗包含了一些在 ES3 中保留、但自 ES5 起不再保留的词byte、long等。7.1 属性名/对象键的限制已解除在 ES5 之前保留字也不能用作属性名或对象字面量的键这个限制现已不存在。因此以下写法不允许var import 42;但以下写法允许var obj { import: 42 }; console.log( obj.import );不过要意识到一些较老的浏览器版本主要是老版本 IE在应用这些规则上并不完全一致因此在对象属性名位置使用保留字仍可能在某些环境下引发问题。请仔细测试所有受支持的浏览器环境。八、实现限制Implementation Limits规范没说但引擎有JS 规范并没有对诸如函数参数个数或字符串字面量长度设置人为上限但这些限制依然存在——因为不同引擎的实现细节不同。例如function addAll() { var sum 0; for (var i0; i arguments.length; i) { sum arguments[i]; } return sum; } var nums []; for (var i1; i 100000; i) { nums.push(i); } addAll( 2, 4, 6 ); // 12 addAll.apply( null, nums ); // 应为: 499950000在某些 JS 引擎中你会得到正确的499950000但在另一些引擎如 Safari 6.x中你会得到错误RangeError: Maximum call stack size exceeded.最大调用栈溢出。已知存在限制的其他例子包括字符串字面量不仅仅是字符串值中允许的最大字符数函数调用参数中可发送的数据大小即栈大小以字节计函数声明中的参数数量非优化调用栈的最大深度例如递归场景函数调用链能有多长JS 程序可连续运行而阻塞浏览器的秒数变量名的最大允许长度……碰到这些限制的情况并不常见但你应该知道限制确实存在且在不同引擎之间各不相同。九、回顾与总结我们可以确信JS 语言本身只有一份标准并且被所有现代浏览器/引擎可预测地实现——这是非常好的事情但 JavaScript 很少孤立运行它运行在与第三方库代码混合的环境中有时甚至运行在与浏览器不同的引擎/环境中。密切关注这些问题能显著提高你代码的可靠性与健壮性。实践清单速查主题关键结论应对策略Annex B / Web ECMAScript浏览器为兼容而存在的规范偏差八进制字面量、escape/unescape、substr、HTML 字符串方法、RegExp.$1..$9等仅在明确仅运行于浏览器时才可依赖尽量不使用Host Objects环境提供的对象DOM 元素、console有特殊[[Class]]、只读属性、this绑定限制交互时不要假设其行为与普通object一致Global DOM VariablesHTML 元素的id会创建同名的全局变量避免使用全局变量名称要避开 HTML 内容与其他代码Native Prototypes扩展原生原型可能在未来规范落地时与你冲突不要扩展必要时用带if守卫的 polyfill/shim 并做好合规测试与文档script标签各 script 是独立程序共享global但不共享提升错误相互隔离注意跨 script 的声明顺序动态注入时规避/script序列Reserved Words关键字/未来保留字不能作变量名但可作对象键注意老版本 IE 的不一致Implementation Limits引擎对参数个数、字符串长度、调用栈深度等有隐藏上限避免超大参数集如apply海量参数与无限递归延伸阅读本仓库内types grammar/README.md本卷目录与全书结构types grammar/toc.md全书各章总览types grammar/ch1.md类型与typeof基础types grammar/ch2.md值数组、字符串、数字等的细节types grammar/ch3.mdNatives 与内部[[Class]]机制types grammar/ch4.md强制转换与 Falsy Objectsdocument.all案例types grammar/ch5.md语法与语句。赞分享教程文档【免费下载链接】you-dont-know-js-ru Russian translation of You Dont Know JS book series项目地址https://gitcode.com/gh_mirrors/yo/you-dont-know-js-ru点击查看免费下载相关推荐Jetifier-standalone 使用指南JAR/AAR/ZIP 文件的 AndroidX 转换Jetifier standalone 使用指南JAR/AAR/ZIP 文件的 AndroidX 转换 Jetifier standalone 是一款强大的上一篇AReaL-SEA模型微调如何基于自己的数据进行定制化训练下一篇突破性工具深度解析如何实现Evernote到Markdown的高效格式转换创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →