当前位置: 首页 > news >正文

平台公司是什么意思aso优化平台有哪些

平台公司是什么意思,aso优化平台有哪些,珠海集团网站建设外包,seo扣费系统源码第十六章#xff1a;变量#xff1a;作用域、环境和闭包 原文#xff1a;16. Variables: Scopes, Environments, and Closures 译者#xff1a;飞龙 协议#xff1a;CC BY-NC-SA 4.0 本章首先解释了如何使用变量#xff0c;然后详细介绍了它们的工作方式#xff08;环境…第十六章变量作用域、环境和闭包 原文16. Variables: Scopes, Environments, and Closures 译者飞龙 协议CC BY-NC-SA 4.0 本章首先解释了如何使用变量然后详细介绍了它们的工作方式环境、闭包等。 声明变量 在 JavaScript 中您在使用变量之前通过var语句声明变量 var foo; foo 3; // OK, has been declared bar 5; // not OK, an undeclared variable您还可以将声明与赋值结合在一起立即初始化变量 var foo 3;未初始化变量的值为undefined var x;x undefined背景静态与动态 您可以从两个角度来检查程序的工作方式 静态或词法 您在不运行程序的情况下检查程序的存在。给定以下代码我们可以得出静态断言即函数“g”嵌套在函数“f”内部 function f() {function g() {} }形容词词法与静态是同义词因为两者都与程序的词汇单词源代码有关。 动态 您在执行程序时检查发生的情况“在运行时”。给定以下代码 function g() { } function f() {g(); }当我们调用f()时它调用g()。在运行时g被f调用表示动态关系。 背景变量的范围 在本章的其余部分您应该了解以下概念 变量的范围 变量的范围是它可访问的位置。例如 function foo() {var x; }在这里“x”的直接作用域是函数“foo()”。 词法作用域 JavaScript 中的变量是词法作用域的因此程序的静态结构决定了变量的作用域不受例如函数从何处调用的影响。 嵌套范围 如果作用域嵌套在变量的直接作用域内则该变量在所有这些作用域中都是可访问的 function foo(arg) {function bar() {console.log(arg: arg);}bar(); } console.log(foo(hello)); // arg: hello“arg”的直接范围是“foo()”但它也可以在嵌套范围“bar()”中访问。就嵌套而言“foo()”是外部范围“bar()”是内部范围。 遮蔽 如果作用域声明了与外部作用域中的变量同名的变量则内部作用域中将阻止对外部变量的访问并且所有嵌套在其中的作用域。对内部变量的更改不会影响外部变量在离开内部作用域后外部变量再次可访问 var x global; function f() {var x local;console.log(x); // local } f(); console.log(x); // global在函数“f()”内部全局“x”被本地“x”遮蔽。 变量是函数作用域的 大多数主流语言都是块作用域的变量“存在于”最内部的周围代码块中。以下是 Java 的一个例子 public static void main(String[] args) {{ // block startsint foo 4;} // block endsSystem.out.println(foo); // Error: cannot find symbol }在前面的代码中变量foo只能在直接包围它的块内部访问。如果我们在块结束后尝试访问它将会得到编译错误。 相比之下JavaScript 的变量是函数作用域的只有函数引入新的作用域在作用域方面忽略了块。例如 function main() {{ // block startsvar foo 4;} // block endsconsole.log(foo); // 4 }换句话说“foo”在“main()”中是可访问的而不仅仅是在块内部。 变量声明被提升 JavaScript 提升所有变量声明将它们移动到其直接作用域的开头。这样可以清楚地说明如果在声明之前访问变量会发生什么 function f() {console.log(bar); // undefinedvar bar abc;console.log(bar); // abc }我们可以看到变量“bar”已经存在于“f()”的第一行但它还没有值也就是说声明已经被提升但赋值没有。JavaScript 执行f()时就好像它的代码是 function f() {var bar;console.log(bar); // undefinedbar abc;console.log(bar); // abc }如果声明已经声明了一个变量那么什么也不会发生变量的值不变 var x 123;var x;x 123每个函数声明也会被提升但方式略有不同。完整的函数会被提升而不仅仅是存储它的变量的创建参见提升。 最佳实践了解提升但不要害怕它 一些 JavaScript 风格指南建议您只在函数开头放置变量声明以避免被提升所欺骗。如果您的函数相对较小无论如何都应该是这样那么您可以放松这个规则将变量声明在使用它们的地方附近例如在for循环内部。这样更好地封装了代码片段。显然您应该意识到这种封装只是概念上的因为函数范围的提升仍然会发生。 陷阱给未声明的变量赋值会使其成为全局变量 在松散模式下对未经var声明的变量进行赋值会创建一个全局变量 function sloppyFunc() { x 123 }sloppyFunc()x 123值得庆幸的是严格模式在发生这种情况时会抛出异常 function strictFunc() { use strict; x 123 }strictFunc() ReferenceError: x is not defined通过 IIFE 引入新的作用域 通常您会引入新的作用域来限制变量的生命周期。一个例子是您可能希望在if语句的“then”部分中这样做只有在条件成立时才执行如果它专门使用辅助变量我们不希望它们“泄漏”到周围的作用域中 function f() {if (condition) {var tmp ...;...}// tmp still exists here// not what we want }如果要为then块引入新的作用域可以定义一个函数并立即调用它。这是一种解决方法模拟块作用域 function f() {if (condition) {(function () { // open blockvar tmp ...;...}()); // close block} }这是 JavaScript 中的一种常见模式。Ben Alman 建议将其称为立即调用函数表达式IIFE发音为“iffy”。一般来说IIFE 看起来像这样 (function () { // open IIFE// inside IIFE }()); // close IIFE以下是关于 IIFE 的一些注意事项 它立即被调用 在函数的闭括号后面的括号立即调用它。这意味着它的主体立即执行。 它必须是一个表达式 如果语句以关键字function开头解析器会期望它是一个函数声明参见Expressions Versus Statements。但是函数声明不能立即调用。因此我们通过以开括号开始语句来告诉解析器关键字function是函数表达式的开始。在括号内只能有表达式。 需要分号 如果您在两个 IIFE 之间忘记了它那么您的代码将不再起作用 (function () {... }()) // no semicolon (function () {... }());前面的代码被解释为函数调用——第一个 IIFE包括括号是要调用的函数第二个 IIFE 是参数。 注意 IIFE 会产生成本在认知和性能方面因此在if语句内部使用它很少有意义。上面的例子是为了教学目的而选择的。 IIFE 变体前缀运算符 您还可以通过前缀运算符强制执行表达式上下文。例如您可以通过逻辑非运算符这样做 !function () { // open IIFE// inside IIFE }(); // close IIFE或通过void运算符参见The void Operator void function () { // open IIFE// inside IIFE }(); // close IIFE使用前缀运算符的优点是忘记结束分号不会引起麻烦。 IIFE 变体已经在表达式上下文中 请注意如果您已经处于表达式上下文中则不需要强制执行 IIFE 的表达式上下文。然后您不需要括号或前缀运算符。例如 var File function () { // open IIFEvar UNTITLED Untitled;function File(name) {this.name name || UNTITLED;}return File; }(); // close IIFE在上面的例子中有两个不同的变量名为File。一方面有一个函数只能直接在 IIFE 内部访问。另一方面在第一行声明的变量。它被赋予在 IIFE 中返回的值。 IIFE 变体带参数的 IIFE 您可以使用参数来定义 IIFE 内部的变量 var x 23; (function (twice) {console.log(twice); }(x * 2));这类似于 var x 23; (function () {var twice x * 2;console.log(twice); }());IIFE 应用 IIFE 使您能够将私有数据附加到函数上。然后您就不必声明全局变量并且可以将函数与其状态紧密打包。您避免了污染全局命名空间 var setValue function () {var prevValue;return function (value) { // define setValueif (value ! prevValue) {console.log(Changed: value);prevValue value;}}; }();IIFE 的其他应用在本书的其他地方提到 避免全局变量隐藏全局范围内的变量参见Best Practice: Avoid Creating Global Variables 创建新的环境避免共享参见Pitfall: Inadvertently Sharing an Environment 将全局数据私有化到所有构造函数中参见Keeping global data private to all of a constructor 将全局数据附加到单例对象上参见[将私有全局数据附加到单例对象](ch17_split_001.html#private_data_singleton “将私有全局数据附加到单例对象” 将全局数据附加到方法参见[将全局数据附加到方法](ch17_split_001.html#private_data_method “将全局数据附加到方法” 全局变量 包含程序的所有范围称为全局范围或程序范围。这是当进入脚本时所在的范围无论是网页中的script标签还是*.js*文件。在全局范围内你可以通过定义一个函数来创建一个嵌套作用域。在这样的函数内部你可以再次嵌套作用域。每个作用域都可以访问自己的变量以及包围它的作用域中的变量。由于全局范围包围所有其他作用域它的变量可以在任何地方访问 // here we are in global scope var globalVariable xyz; function f() {var localVariable true;function g() {var anotherLocalVariable 123;// All variables of surround scopes are accessiblelocalVariable false;globalVariable abc;} } // here we are again in global scope最佳实践避免创建全局变量 全局变量有两个缺点。首先依赖全局变量的软件部分会受到副作用的影响它们不够健壮行为不够可预测也不够可重用。 其次网页上的所有 JavaScript 共享相同的全局变量你的代码内置函数分析代码社交媒体按钮等等。这意味着名称冲突可能会成为一个问题。这就是为什么最好尽可能隐藏全局范围内的变量。例如不要这样做 !-- Don’t do this -- script// Global scopevar tmp generateData();processData(tmp);persistData(tmp); /script变量tmp变成了全局变量因为它的声明是在全局范围内执行的。但它只在本地使用。因此我们可以使用 IIFE参见[通过 IIFE 引入新作用域](ch16.html#iife “通过 IIFE 引入新作用域”将其隐藏在嵌套作用域中 script(function () { // open IIFE// Local scopevar tmp generateData();processData(tmp);persistData(tmp);}()); // close IIFE /script模块系统导致全局变量减少 值得庆幸的是模块系统参见[模块系统](ch31.html#module_systems “模块系统”大多消除了全局变量的问题因为模块不通过全局范围进行接口并且每个模块都有自己的模块全局变量的作用域。 全局对象 ECMAScript 规范使用内部数据结构环境来存储变量参见[环境管理变量](ch16.html#environments “环境管理变量”。该语言具有一个有点不寻常的特性即通过对象所谓的全局对象使全局变量的环境可访问。全局对象可用于创建、读取和更改全局变量。在全局范围内this指向它 var foo hello;this.foo // read global variable hello this.bar world; // create global variablebar world请注意全局对象具有原型。如果你想列出它的自己的和继承的所有属性你需要一个诸如getAllPropertyNames()的函数来自[列出所有属性键](ch17_split_000.html#getAllPropertyNames “列出所有属性键” getAllPropertyNames(window).sort().slice(0, 5) [ AnalyserNode, Array, ArrayBuffer, Attr, Audio ]JavaScript 的创造者 Brendan Eich 认为全局对象是他的“最大遗憾”之一。它对性能产生负面影响使变量作用域的实现更加复杂并导致代码模块化程度降低。 跨平台考虑 浏览器和 Node.js 都有用于引用全局对象的全局变量。不幸的是它们是不同的 浏览器包含window它作为文档对象模型DOM的一部分进行了标准化而不是作为 ECMAScript 5 的一部分。每个框架或窗口都有一个全局对象。 Node.js 包含global这是一个特定于 Node.js 的变量。每个模块都有自己的作用域其中this指向具有该作用域变量的对象。因此在模块内部this和global是不同的。 在两个平台上this都指向全局对象但只有在全局范围内才是如此。这在 Node.js 上几乎从不会发生。如果你想以跨平台的方式访问全局对象可以使用以下模式 (function (glob) {// glob points to global object }(typeof window ! undefined ? window : global));从现在开始我使用window来指代全局对象但在跨平台代码中你应该使用前面的模式和glob。 使用window的用例 本节描述了通过window访问全局变量的用例。但一般规则是尽量避免这样做。 使用情况标记全局变量 前缀window是代码引用全局变量而不是局部变量的视觉线索 var foo 123; (function () {console.log(window.foo); // 123 }());但是这会使您的代码变得脆弱。一旦您将foo从全局范围移动到另一个周围范围它就会停止工作 (function () {var foo 123;console.log(window.foo); // undefined }());因此最好将foo作为变量而不是window的属性引用。如果您想明显地表明foo是全局或类似全局的变量可以添加一个名称前缀例如g_ var g_foo 123; (function () {console.log(g_foo); }());使用情况内置 我不喜欢通过window引用内置全局变量。它们是众所周知的名称因此从指示它们是全局的角度获得的帮助很少。而且前缀的window会增加混乱 window.isNaN(...) // no isNaN(...) // yes使用情况样式检查器 当您使用诸如 JSLint 和 JSHint 之类的样式检查工具时使用window意味着在引用当前文件中未声明的全局变量时不会出错。但是这两个工具都提供了方法来告诉它们这样的变量并防止出现此类错误在其文档中搜索“全局变量”。 使用情况检查全局变量是否存在 这不是一个常见的用例但是 shim 和 polyfill 特别需要检查全局变量someVariable是否存在请参阅Shims Versus Polyfills。在这种情况下window有所帮助 if (window.someVariable) { ... }这是执行此检查的安全方式。如果someVariable未声明则以下语句会引发异常 // Don’t do this if (someVariable) { ... }您可以通过window进行两种额外的检查方式它们大致等效但更加明确 if (window.someVariable ! undefined) { ... } if (someVariable in window) { ... }检查变量是否存在并具有值的一般方法是通过typeof请参阅typeof: Categorizing Primitives if (typeof someVariable ! undefined) { ... }使用情况在全局范围创建事物 window允许您将事物添加到全局范围即使您处于嵌套范围并且它允许您有条件地这样做 if (!window.someApiFunction) {window.someApiFunction ...; }通常最好通过var将事物添加到全局范围而您处于全局范围。但是window提供了一种有条件地进行添加的清晰方式。 环境管理变量 提示 环境是一个高级主题。它们是 JavaScript 内部的细节。如果您想更深入地了解变量的工作原理请阅读本节。 当程序执行进入其作用域时变量就会出现。然后它们需要存储空间。提供该存储空间的数据结构在 JavaScript 中称为环境。它将变量名映射到值。其结构与 JavaScript 对象的结构非常相似。环境有时会在您离开其作用域后继续存在。因此它们存储在堆上而不是栈上。 变量以两种方式传递。如果您愿意的话它们有两个维度 动态维度调用函数 每次调用函数时它都需要为其参数和变量提供新的存储空间。完成后该存储通常可以被回收。例如考虑阶乘函数的以下实现。它递归调用自身多次每次都需要n的新存储空间 function fac(n) {if (n 1) {return 1;}return n * fac(n - 1); }词法静态维度保持与周围作用域的连接 无论函数被调用多少次它总是需要访问自己新鲜的局部变量和周围作用域的变量。例如以下函数doNTimes内部有一个辅助函数doNTimesRec。当doNTimesRec多次调用自身时每次都会创建一个新的环境。但是在这些调用期间doNTimesRec也保持与doNTimes的单个环境的连接类似于所有函数共享单个全局环境。doNTimesRec需要该连接来访问1行中的action function doNTimes(n, action) {function doNTimesRec(x) {if (x 1) {action(); // (1)doNTimesRec(x-1);}}doNTimesRec(n); }这两个维度的处理方式如下 动态维度执行上下文的堆栈 每次调用函数时都会创建一个新的环境用于将标识符参数和变量映射到值。为了处理递归执行上下文——对环境的引用——在堆栈中进行管理。该堆栈反映了调用堆栈。 词法维度环境链 为了支持这个维度函数通过内部属性[[Scope]]记录了它被创建时的作用域。当调用函数时会为进入的新作用域创建一个环境。该环境有一个称为outer的字段指向外部作用域的环境并通过[[Scope]]设置。因此始终存在一条环境链从当前活动环境开始继续到它的外部环境依此类推。每条链以全局环境结束最初调用函数的作用域。全局环境的outer字段为null。 为了解析标识符会遍历完整的环境链从活动环境开始。 让我们看一个例子 function myFunction(myParam) {var myVar 123;return myFloat; } var myFloat 1.3; // Step 1 myFunction(abc); // Step 2图 16-1变量的动态维度通过执行上下文的堆栈处理静态维度通过环境链处理。活动的执行上下文、环境和函数被突出显示。步骤 1 显示了在调用 myFunction(abc)函数之前这些数据结构。步骤 2 显示了在函数调用期间。 图 16-1 函数之前这些数据结构。步骤 2 显示了在函数调用期间。)说明了在执行前述代码时会发生什么 myFunction和myFloat已经存储在全局环境#0中。请注意由myFunction引用的function对象通过内部属性[[Scope]]指向它的作用域全局作用域。 对于执行myFunction(abc)会创建一个新的环境#1其中包含参数和局部变量。它通过outer从myFunction.[[Scope]]初始化引用其外部环境。由于外部环境myFunction可以访问myFloat。 闭包函数保持与它们诞生作用域的连接 如果一个函数离开了它被创建的作用域它仍然与该作用域以及周围作用域的变量保持连接。例如 function createInc(startValue) {return function (step) {startValue step;return startValue;}; }createInc()返回的函数不会失去与startValue的连接——该变量为函数提供了跨函数调用持续存在的状态 var inc createInc(5);inc(1) 6inc(2) 8闭包是一个函数加上它被创建时的作用域的连接。这个名字源于闭包“封闭”了函数的自由变量。如果一个变量不是在函数内声明的即来自“外部”那么它就是自由的。 通过环境处理闭包 提示 这是一个深入探讨闭包工作原理的高级部分。您应该熟悉环境请查看环境管理变量。 闭包是执行离开其范围后仍然存在的环境的一个例子。为了说明闭包的工作原理让我们将之前与createInc()的交互分解为四个步骤在每个步骤中突出显示活动执行上下文及其环境如果函数是活动的它也会被突出显示 这一步发生在交互之前并且在评估createInc的函数声明之后。createInc的条目已添加到全局环境0中并指向一个函数对象。 这一步发生在函数调用createInc(5)期间。为createInc创建了一个新的环境1并推送到堆栈上。它的外部环境是全局环境与createInc.[[Scope]]相同。该环境保存了参数startValue。 这一步发生在对inc进行赋值之后。当我们从createInc返回时指向其环境的执行上下文已从堆栈中移除但是环境仍然存在于堆中因为inc.[[Scope]]引用它。inc是一个闭包函数加出生环境。 这一步发生在执行inc(1)期间。已创建了一个新的环境1并且指向它的执行上下文已被推送到堆栈上。它的外部环境是inc的[[Scope]]。外部环境使inc可以访问startValue。 这一步发生在执行inc(1)之后。不再有引用执行上下文outer字段或[[Scope]]指向inc的环境。因此它不再需要并且可以从堆中删除。 陷阱无意中共享环境 有时您创建的函数的行为受当前范围内的变量的影响。在 JavaScript 中这可能会有问题因为每个函数应该使用函数创建时变量的值。但是由于函数是闭包函数将始终使用变量的当前值。在for循环中这可能会导致事情无法正常工作。通过示例可以更清楚地说明 function f() {var result [];for (var i0; i3; i) {var func function () {return i;};result.push(func);}return result; } console.log(f()1; // 3f返回一个包含三个函数的数组。所有这些函数仍然可以访问f的环境因此也可以访问i。实际上它们共享相同的环境。然而在循环结束后i在该环境中的值为 3。因此所有函数都返回3。 这不是我们想要的。为了解决问题我们需要在创建使用它的函数之前对索引i进行快照。换句话说我们希望将每个函数与函数创建时i的值打包在一起。因此我们采取以下步骤 为返回的数组中的每个函数创建一个新的环境。 在该环境中存储复制i的当前值。 只有函数创建环境因此我们使用 IIFE参见通过 IIFE 引入新作用域来完成第 1 步 function f() {var result [];for (var i0; i3; i) {(function () { // step 1: IIFEvar pos i; // step 2: copyvar func function () {return pos;};result.push(func);}());}return result; } console.log(f()1; // 1请注意该示例具有现实世界的相关性因为在通过循环向 DOM 元素添加事件处理程序时会出现类似的情况。 第十七章对象和继承 原文17. Objects and Inheritance 译者飞龙 协议CC BY-NC-SA 4.0 JavaScript 中的面向对象编程OOP有几个层次 第 1 层使用单个对象的面向对象在第 1 层单个对象中介绍 第二层对象的原型链在第二层对象之间的原型关系中描述 第 3 层构造函数作为实例的工厂类似于其他语言中的类在第 3 层构造函数—实例的工厂中讨论 第 4 层子类化通过从现有构造函数继承创建新的构造函数在第 4 层构造函数之间的继承中讨论 每个新层只依赖于之前的层使您能够逐步学习 JavaScript OOP。第 1 层和第 2 层形成一个简单的核心您可以在更复杂的第 3 层和第 4 层让您感到困惑时进行参考。 第 1 层单个对象 大致上JavaScript 中的所有对象都是从字符串到值的映射字典。对象中的键值条目称为属性。属性的键始终是文本字符串。属性的值可以是任何 JavaScript 值包括函数。方法是其值为函数的属性。 属性的种类 有三种属性 属性或命名数据属性 对象中的普通属性—即从字符串键到值的映射。命名数据属性包括方法。这是最常见的属性类型。 访问器或命名访问器属性 特殊方法的调用看起来像是读取或写入属性。普通属性是属性值的存储位置访问器允许您计算属性的值。如果你愿意它们是虚拟属性。有关详细信息请参见访问器getter 和 setter。 内部属性 仅存在于 ECMAScript 语言规范中。它们无法直接从 JavaScript 中访问但可能有间接访问它们的方法。规范使用方括号写入内部属性的键。例如[[Prototype]]保存对象的原型并且可以通过Object.getPrototypeOf()读取。 对象字面量 JavaScript 的对象字面量允许您直接创建普通对象Object的直接实例。以下代码使用对象字面量将对象分配给变量jane。对象具有两个属性name和describe。describe是一个方法 var jane {name: Jane,describe: function () {return Person named this.name; // (1)}, // (2) };在方法中使用this来引用当前对象也称为方法调用的接收者。 ECMAScript 5 允许在对象字面量中使用尾随逗号在最后一个属性之后。遗憾的是并非所有旧版浏览器都支持它。尾随逗号很有用因为您可以重新排列属性而不必担心哪个属性是最后一个。 您可能会有这样的印象即对象只是从字符串到值的映射。但它们不仅如此它们是真正的通用对象。例如您可以在对象之间使用继承请参见第 2 层对象之间的原型关系并且可以保护对象免受更改。直接创建对象的能力是 JavaScript 的一大特点您可以从具体对象开始无需类然后稍后引入抽象。例如构造函数是对象的工厂如第 3 层构造函数—实例的工厂中讨论的大致类似于其他语言中的类。 点运算符.通过固定键访问属性 点运算符提供了一种紧凑的语法来访问属性。属性键必须是标识符参见Legal Identifiers。如果您想要读取或写入具有任意名称的属性您需要使用括号运算符参见Bracket Operator ([]): Accessing Properties via Computed Keys。 本节中的示例与以下对象一起使用 var jane {name: Jane,describe: function () {return Person named this.name;} };获取属性 点运算符允许您“获取”属性读取其值。以下是一些示例 jane.name // get property name Janejane.describe // get property describe [Function]获取不存在的属性返回undefined jane.unknownProperty undefined调用方法 点运算符也用于调用方法 jane.describe() // call method describe Person named Jane设置属性 您可以使用赋值运算符通过点表示法设置属性的值。例如 jane.name John; // set property namejane.describe() Person named John如果属性尚不存在则设置它会自动创建它。如果属性已经存在则设置它会更改其值。 删除属性 delete运算符允许您完全从对象中删除属性整个键值对。例如 var obj { hello: world };delete obj.hello trueobj.hello undefined如果您仅将属性设置为undefined则该属性仍然存在对象仍然包含其键 var obj { foo: a, bar: b }; obj.foo undefined;Object.keys(obj) [ foo, bar ]如果删除属性则其键也将消失 delete obj.foo trueObject.keys(obj) [ bar ]delete仅影响对象的直接“自有”非继承的属性。其原型不会受到影响参见Deleting an inherited property。 提示 谨慎使用delete运算符。大多数现代 JavaScript 引擎会优化通过构造函数创建的实例的性能如果它们的“形状”不发生变化大致上不会删除或添加属性。删除属性会阻止该优化。 delete的返回值 如果属性是自有属性但无法删除则delete返回false。在所有其他情况下它返回true。以下是一些示例。 作为准备工作我们创建一个可以删除的属性和另一个无法删除的属性Getting and Defining Properties via Descriptors解释了Object.defineProperty() var obj {}; Object.defineProperty(obj, canBeDeleted, {value: 123,configurable: true }); Object.defineProperty(obj, cannotBeDeleted, {value: 456,configurable: false });对于无法删除的自有属性delete返回false delete obj.cannotBeDeleted false在所有其他情况下delete都会返回true delete obj.doesNotExist truedelete obj.canBeDeleted true即使不改变任何内容delete也会返回true继承属性永远不会被删除 delete obj.toString trueobj.toString // still there [Function: toString]不寻常的属性键 虽然您不能将保留字例如var和function用作变量名但您可以将它们用作属性键 var obj { var: a, function: b };obj.var aobj.function b数字可以在对象文字中用作属性键但它们被解释为字符串。点运算符只能访问其键为标识符的属性。因此您需要使用括号运算符如下例所示来访问其键为数字的属性 var obj { 0.7: abc };Object.keys(obj) [ 0.7 ]obj[0.7] abc对象文字还允许您使用任意字符串既不是标识符也不是数字作为属性键但您必须对其进行引用。同样您需要使用括号运算符来访问属性值 var obj { not an identifier: 123 };Object.keys(obj) [ not an identifier ]obj[not an identifier] 123括号运算符[]通过计算键访问属性 虽然点运算符适用于固定属性键但括号运算符允许您通过表达式引用属性。 通过括号运算符获取属性 括号运算符允许您通过表达式计算属性的键 var obj { someProperty: abc }; obj[some Property] abc var propKey someProperty;obj[propKey] abc这也允许您访问其键不是标识符的属性 var obj { not an identifier: 123 };obj[not an identifier] 123请注意括号运算符会将其内部强制转换为字符串。例如 var obj { 6: bar };obj[33] // key: the string 6 bar通过括号运算符调用方法 调用方法的工作方式与您期望的一样 var obj { myMethod: function () { return true } };obj[myMethod]() true通过括号运算符设置属性 设置属性的工作方式与点运算符类似 var obj {};obj[anotherProperty] def;obj.anotherProperty def通过括号运算符删除属性 删除属性的工作方式也与点运算符类似 var obj { not an identifier: 1, prop: 2 };Object.keys(obj) [ not an identifier, prop ]delete obj[not an identifier] trueObject.keys(obj) [ prop ]将任何值转换为对象 这并不是一个常见的用例但有时你需要将任意值转换为对象。Object() 作为函数而不是构造函数提供了这种服务。它产生以下结果 值结果(不带参数调用){}undefined{}null{}布尔值 boolnew Boolean(bool)数字 numnew Number(num)字符串 strnew String(str)对象 objobj不变无需转换 以下是一些例子 Object(null) instanceof Object true Object(false) instanceof Boolean true var obj {};Object(obj) obj true以下函数检查 value 是否为对象 function isObject(value) {return value Object(value); }请注意如果 value 不是对象则前面的函数将创建一个对象。你可以通过 typeof 实现相同的功能而不需要这样做参见Pitfall: typeof null。 你也可以将 Object 作为构造函数调用这将产生与作为函数调用相同的结果 var obj {};new Object(obj) obj true new Object(123) instanceof Number true提示 避免使用构造函数几乎总是更好的选择是一个空对象字面量 var obj new Object(); // avoid var obj {}; // prefer函数和方法的隐式参数 this 当你调用一个函数时this 总是一个隐式参数 松散模式下的普通函数 即使普通函数对 this 没有用处它仍然存在作为一个特殊变量其值始终是全局对象在浏览器中是 window参见全局对象 function returnThisSloppy() { return this }returnThisSloppy() window true严格模式下的普通函数 this 总是 undefined function returnThisStrict() { use strict; return this }returnThisStrict() undefined true方法 this 指的是调用方法的对象 var obj { method: returnThisStrict };obj.method() obj true在方法的情况下this 的值被称为方法调用的接收者。 在设置 this 的情况下调用函数call()、apply() 和 bind() 记住函数也是对象。因此每个函数都有自己的方法。本节介绍了其中三个方法并帮助调用函数。这三种方法在以下部分中用于解决调用函数的一些陷阱。即将出现的示例都涉及以下对象 jane var jane {name: Jane,sayHelloTo: function (otherName) {use strict;console.log(this.name says hello to otherName);} };Function.prototype.call(thisValue, arg1?, arg2?, …) 第一个参数是被调用函数内部的 this 的值其余参数作为参数传递给被调用的函数。以下三次调用是等价的 jane.sayHelloTo(Tarzan);jane.sayHelloTo.call(jane, Tarzan);var func jane.sayHelloTo; func.call(jane, Tarzan);对于第二次调用你需要重复 jane因为 call() 不知道你是如何得到它被调用的函数的。 Function.prototype.apply(thisValue, argArray) 第一个参数是被调用函数内部的 this 的值第二个参数是一个提供调用参数的数组。以下三次调用是等价的 jane.sayHelloTo(Tarzan);jane.sayHelloTo.apply(jane, [Tarzan]);var func jane.sayHelloTo; func.apply(jane, [Tarzan]);对于第二次调用你需要重复 jane因为 apply() 不知道你是如何得到它被调用的函数的。 用于构造函数的 apply() 解释了如何在构造函数中使用 apply()。 Function.prototype.bind(thisValue, arg1?, …, argN?) 这个方法执行部分函数应用——意味着它创建一个新的函数以以下方式调用 bind() 的接收者this 的值是 thisValue参数从 arg1 开始直到 argN然后是新函数的参数。换句话说当新函数调用原始函数时它将其参数附加到 arg1, ..., argN。让我们看一个例子 function func() {console.log(this: this);console.log(arguments: Array.prototype.slice.call(arguments)); } var bound func.bind(abc, 1, 2);数组方法 slice 用于将 arguments 转换为数组这对于记录它是必要的这个操作在类数组对象和通用方法中有解释。bound 是一个新函数。以下是交互 bound(3) this: abc arguments: 1,2,3以下三次对 sayHelloTo 的调用都是等价的 jane.sayHelloTo(Tarzan);var func1 jane.sayHelloTo.bind(jane); func1(Tarzan);var func2 jane.sayHelloTo.bind(jane, Tarzan); func2();用于构造函数的 apply() 假设 JavaScript 有一个三个点运算符...可以将数组转换为实际参数。这样的运算符将允许您使用Math.max()参见其他函数与数组。在这种情况下以下两个表达式将是等价的 Math.max(...[13, 7, 30]) Math.max(13, 7, 30)对于函数您可以通过apply()实现三个点运算符的效果 Math.max.apply(null, [13, 7, 30]) 30三个点运算符对构造函数也有意义 new Date(...[2011, 11, 24]) // Christmas Eve 2011遗憾的是这里apply()不起作用因为它只能帮助函数或方法调用而不能帮助构造函数调用。 手动模拟构造函数的 apply() 我们可以分两步模拟apply()。 步骤 1 通过方法调用将参数传递给Date它们还不在数组中 new (Date.bind(null, 2011, 11, 24))前面的代码使用bind()创建一个没有参数的构造函数并通过new调用它。 步骤 2 使用apply()将数组传递给bind()。因为bind()是一个方法调用所以我们可以使用apply() new (Function.prototype.bind.apply(Date, [null, 2011, 11, 24]))前面的数组仍然有一个多余的元素null。我们可以使用concat()来添加 var arr [2011, 11, 24]; new (Function.prototype.bind.apply(Date, [null].concat(arr)))一个库方法 前面的手动解决方法是受到 Mozilla 发布的库方法的启发。以下是它的一个稍微编辑过的版本 if (!Function.prototype.construct) {Function.prototype.construct function(argArray) {if (! Array.isArray(argArray)) {throw new TypeError(Argument must be an array);}var constr this;var nullaryFunc Function.prototype.bind.apply(constr, [null].concat(argArray));return new nullaryFunc();}; }这是使用的方法 Date.construct([2011, 11, 24]) Sat Dec 24 2011 00:00:00 GMT0100 (CET)另一种方法 与之前的方法相比的另一种方法是通过Object.create()创建一个未初始化的实例然后通过apply()调用构造函数作为函数。这意味着您实际上正在重新实现new运算符一些检查被省略 Function.prototype.construct function(argArray) {var constr this;var inst Object.create(constr.prototype);var result constr.apply(inst, argArray); // (1)// Check: did the constructor return an object// and prevent this from being the result?return result ? result : inst; };警告 前面的代码对于大多数内置构造函数都不起作用当作为函数调用时总是产生新的实例。换句话说(1)处的步骤没有设置inst为期望的值。 陷阱提取方法时丢失 this 如果您从对象中提取一个方法它将再次成为一个真正的函数。它与对象的连接被切断通常不再正常工作。例如考虑以下对象counter var counter {count: 0,inc: function () {this.count;} }提取inc并调用它作为函数失败 var func counter.inc;func()counter.count // didn’t work 0这是解释我们已经将counter.inc的值作为函数调用。因此this是全局对象我们执行了window.count。window.count不存在是undefined。对它应用运算符会将其设置为NaN count // global variable NaN如何获得警告 如果方法inc()处于严格模式您会收到一个警告 counter.inc function () { use strict; this.count };var func2 counter.inc;func2() TypeError: Cannot read property count of undefined原因是当我们调用严格模式函数func2时this是undefined导致错误。 如何正确提取方法 由于bind()我们可以确保inc不会失去与counter的连接 var func3 counter.inc.bind(counter);func3()counter.count // it worked! 1回调和提取的方法 在 JavaScript 中有许多接受回调的函数和方法。浏览器中的示例包括setTimeout()和事件处理。如果我们将counter.inc作为回调传入它也会作为函数调用导致刚才描述的相同问题。为了说明这一现象让我们使用一个简单的回调调用函数 function callIt(callback) {callback(); }通过callIt执行counter.count会触发警告由于严格模式 callIt(counter.inc) TypeError: Cannot read property count of undefined与以前一样我们通过bind()来修复问题 callIt(counter.inc.bind(counter))counter.count // one more than before 2警告 每次调用bind()都会创建一个新的函数。这在注册和注销回调时例如事件处理会产生后果。您需要将注册的值存储在某个地方并且也需要用它来进行注销。 陷阱方法内部的函数会遮蔽 this 您经常在 JavaScript 中嵌套函数定义因为函数可以是参数例如回调并且因为它们可以通过函数表达式在原地创建。当一个方法包含一个普通函数并且您想在后者内部访问前者的this时这会导致问题因为方法的this被普通函数的this遮蔽了后者甚至对自己的this没有任何用处。在以下示例中(1)处的函数尝试访问(2)处的方法的this var obj {name: Jane,friends: [ Tarzan, Cheeta ],loop: function () {use strict;this.friends.forEach(function (friend) { // (1)console.log(this.name knows friend); // (2)});} };显然这会失败因为函数1有自己的this在这里是undefined obj.loop(); TypeError: Cannot read property name of undefined有三种方法可以解决这个问题。 解决方法 1that this 我们将this分配给一个不会在嵌套函数中被遮蔽的变量 loop: function () {use strict;var that this;this.friends.forEach(function (friend) {console.log(that.name knows friend);}); }以下是交互 obj.loop(); Jane knows Tarzan Jane knows Cheeta解决方法 2bind() 我们可以使用bind()为this提供一个固定值即方法的this行1 loop: function () {use strict;this.friends.forEach(function (friend) {console.log(this.name knows friend);}.bind(this)); // (1) }解决方法 3forEach()的 thisValue 针对forEach()参见检查方法的一个特定解决方法是在回调之后提供第二个参数该参数成为回调的this loop: function () {use strict;this.friends.forEach(function (friend) {console.log(this.name knows friend);}, this); }第 2 层对象之间的原型关系 两个对象之间的原型关系涉及继承每个对象都可以有另一个对象作为其原型。然后前一个对象继承其原型的所有属性。对象通过内部属性[[Prototype]]指定其原型。每个对象都有这个属性但它可以是null。通过[[Prototype]]属性连接的对象链称为原型链图 17-1。 图 17-1。原型链。 为了了解基于原型或原型式继承的工作原理让我们看一个例子使用虚构的语法来指定[[Prototype]]属性 var proto {describe: function () {return name: this.name;} }; var obj {[[Prototype]]: proto,name: obj };对象obj从proto继承了属性describe。它还有一个所谓的自有非继承的直接的属性name。 继承 obj继承了属性describe您可以像对象本身拥有该属性一样访问它 obj.describe [Function]每当通过obj访问属性时JavaScript 会从该对象开始搜索并继续搜索其原型、原型的原型等。这就是为什么我们可以通过obj.describe访问proto.describe。原型链的行为就像它是一个单一对象一样。当调用方法时这种幻觉总是保持this的值始终是方法开始搜索的对象而不是找到方法的对象。这允许方法访问原型链的所有属性。例如 obj.describe() name: obj在describe()内部this是obj这允许该方法访问obj.name。 覆盖 在原型链中对象中的属性覆盖了“后来”对象中具有相同键的属性首先找到前者属性。它隐藏了后者属性后者属性无法再被访问。例如让我们在obj中覆盖方法proto.describe() obj.describe function () { return overridden };obj.describe() overridden这类似于类语言中方法覆盖的工作原理。 通过原型在对象之间共享数据 原型非常适合在对象之间共享数据多个对象获得相同的原型其中包含所有共享的属性。让我们看一个例子。对象jane和tarzan都包含相同的方法describe()。这是我们希望通过共享来避免的事情 var jane {name: Jane,describe: function () {return Person named this.name;} }; var tarzan {name: Tarzan,describe: function () {return Person named this.name;} };两个对象都是人。它们的name属性不同但我们可以让它们共享describe方法。我们通过创建一个名为PersonProto的共同原型并将describe放入其中来实现这一点图 17-2。 图 17-2。对象 jane 和 tarzan 共享原型 PersonProto因此共享属性 describe。 以下代码创建了对象jane和tarzan它们共享原型PersonProto var PersonProto {describe: function () {return Person named this.name;} }; var jane {[[Prototype]]: PersonProto,name: Jane }; var tarzan {[[Prototype]]: PersonProto,name: Tarzan };以下是交互 jane.describe() Person named Janetarzan.describe() Person named Tarzan这是一种常见的模式数据驻留在原型链的第一个对象中而方法驻留在后来的对象中。JavaScript 的原型继承设计支持这种模式设置属性仅影响原型链中的第一个对象而获取属性则考虑整个链条参见设置和删除仅影响自有属性。 获取和设置原型 到目前为止我们假装你可以从 JavaScript 中访问内部属性[[Prototype]]。但是语言不允许你这样做。相反有用于读取原型和创建具有给定原型的新对象的函数。 创建具有给定原型的新对象 这个调用 Object.create(proto, propDescObj?)创建一个原型为proto的对象。可以通过描述符添加属性在属性描述符中有解释。在以下示例中对象jane获得了原型PersonProto和一个可变属性name其值为’Jane’通过属性描述符指定 var PersonProto {describe: function () {return Person named this.name;} }; var jane Object.create(PersonProto, {name: { value: Jane, writable: true } });以下是交互 jane.describe() Person named Jane但是你经常只是创建一个空对象然后手动添加属性因为描述符太啰嗦了 var jane Object.create(PersonProto); jane.value Jane;读取对象的原型 这个方法调用 Object.getPrototypeOf(obj)返回obj的原型。继续前面的例子 Object.getPrototypeOf(jane) PersonProto true检查一个对象是否是另一个对象的原型 这种语法 Object.prototype.isPrototypeOf(obj)检查方法的接收者是否是obj的直接或间接原型。换句话说接收者和obj是否在同一个原型链中obj是否在接收者之前例如 var A {};var B Object.create(A);var C Object.create(B);A.isPrototypeOf(C) trueC.isPrototypeOf(A) false查找定义属性的对象 以下函数遍历对象obj的属性链。它返回第一个具有键propKey的自有属性的对象如果没有这样的对象则返回null function getDefiningObject(obj, propKey) {obj Object(obj); // make sure it’s an objectwhile (obj !{}.hasOwnProperty.call(obj, propKey)) {obj Object.getPrototypeOf(obj);// obj is null if we have reached the end}return obj; }在前面的代码中我们通用地调用了方法Object.prototype.hasOwnProperty参见通用方法从原型中借用方法。 特殊属性 proto 一些 JavaScript 引擎有一个特殊的属性用于获取和设置对象的原型__proto__。它为语言带来了对[[Prototype]]的直接访问 var obj {}; obj.__proto__ Object.prototype true obj.__proto__ Array.prototypeObject.getPrototypeOf(obj) Array.prototype true有几件事情你需要知道关于__proto__ __proto__的发音是“dunder proto”是“double underscore proto”的缩写。这种发音是从 Python 编程语言中借来的如 Ned Batchelder 在 2006 年所建议的。在 Python 中双下划线的特殊变量非常频繁。 __proto__不是 ECMAScript 5 标准的一部分。因此如果您希望您的代码符合该标准并且可以在当前的 JavaScript 引擎中可靠运行那么您不应该使用它。 然而越来越多的引擎正在添加对__proto__的支持它将成为 ECMAScript 6 的一部分。 以下表达式检查引擎是否支持__proto__作为特殊属性 Object.getPrototypeOf({ __proto__: null }) null设置和删除仅影响自有属性 只有获取属性才会考虑对象的完整原型链。设置和删除会忽略继承只影响自有属性。 设置属性 设置属性会创建一个自有属性即使存在具有该键的继承属性。例如给定以下源代码 var proto { foo: a }; var obj Object.create(proto);obj从proto继承了foo obj.foo aobj.hasOwnProperty(foo) false设置foo会产生期望的结果 obj.foo b;obj.foo b然而我们创建了一个自有属性而没有改变proto.foo obj.hasOwnProperty(foo) trueproto.foo a其原因是原型属性应该被多个对象共享。这种方法允许我们非破坏性地“改变”它们——只有当前对象受到影响。 删除继承属性 您只能删除自有属性。让我们再次设置一个对象obj并具有原型proto var proto { foo: a }; var obj Object.create(proto);删除继承的属性foo没有效果 delete obj.foo trueobj.foo a有关delete运算符的更多信息请参阅删除属性。 在原型链的任何位置更改属性 如果要更改继承的属性首先必须找到拥有该属性的对象参见查找定义属性的对象然后在该对象上执行更改。例如让我们从前面的示例中删除属性foo delete getDefiningObject(obj, foo).foo; trueobj.foo undefined属性的迭代和检测 迭代和检测属性的操作受以下因素的影响 继承自有属性与继承属性 对象的自有属性直接存储在该对象中。继承的属性存储在其原型之一中。 可枚举性可枚举属性与不可枚举属性 属性的可枚举性是一个属性参见属性特性和属性描述符一个可以是true或false的标志。可枚举性很少重要通常可以忽略参见可枚举性最佳实践。 您可以列出自有属性键列出所有可枚举属性键并检查属性是否存在。以下各小节显示了如何操作。 列出自有属性键 您可以列出所有自有属性键或仅列出可枚举的属性键 Object.getOwnPropertyNames(obj)返回obj的所有自有属性的键。 Object.keys(obj)返回obj的所有可枚举自有属性的键。 请注意属性通常是可枚举的参见可枚举性最佳实践因此您可以使用Object.keys()特别是对于您创建的对象。 列出所有属性键 如果要列出对象的所有属性自有和继承的属性则有两种选择。 选项 1 是使用循环 for («variable» in «object»)«statement»遍历object的所有可枚举属性的键。有关更详细的描述请参见for-in。 选项 2 是自己实现一个函数该函数迭代所有属性而不仅仅是可枚举的属性。例如 function getAllPropertyNames(obj) {var result [];while (obj) {// Add the own property names of obj to resultArray.prototype.push.apply(result, Object.getOwnPropertyNames(obj));obj Object.getPrototypeOf(obj);}return result; }检查属性是否存在 您可以检查对象是否具有属性或者属性是否直接存在于对象内部 propKey in obj 如果obj具有键为propKey的属性则返回true。继承的属性也包括在此测试中。 Object.prototype.hasOwnProperty(propKey) 如果接收者this具有键为propKey的自有非继承属性则返回true。 警告 避免直接在对象上调用hasOwnProperty()因为它可能被覆盖例如由一个键为hasOwnProperty的自有属性 var obj { hasOwnProperty: 1, foo: 2 };obj.hasOwnProperty(foo) // unsafe TypeError: Property hasOwnProperty is not a function相反最好通用调用它参见通用方法从原型中借用方法 Object.prototype.hasOwnProperty.call(obj, foo) // safe true{}.hasOwnProperty.call(obj, foo) // shorter true示例 以下示例基于这些定义 var proto Object.defineProperties({}, {protoEnumTrue: { value: 1, enumerable: true },protoEnumFalse: { value: 2, enumerable: false } }); var obj Object.create(proto, {objEnumTrue: { value: 1, enumerable: true },objEnumFalse: { value: 2, enumerable: false } });Object.defineProperties()在通过描述符获取和定义属性中有解释但它的工作原理应该是相当明显的proto具有自有属性protoEnumTrue和protoEnumFalseobj具有自有属性objEnumTrue和objEnumFalse并继承了proto的所有属性。 注意 请注意对象例如前面示例中的proto通常至少具有原型Object.prototype其中定义了标准方法如toString()和hasOwnProperty() Object.getPrototypeOf({}) Object.prototype true可枚举性的影响 在属性相关的操作中可枚举性只影响for-in循环和Object.keys()它也影响JSON.stringify()参见JSON.stringify(value, replacer?, space?)。 for-in循环遍历所有可枚举属性的键包括继承的属性注意Object.prototype的不可枚举属性都不会显示 for (var x in obj) console.log(x); objEnumTrue protoEnumTrueObject.keys()返回所有自有非继承的可枚举属性的键 Object.keys(obj) [ objEnumTrue ]如果你想要所有自有属性的键你需要使用Object.getOwnPropertyNames() Object.getOwnPropertyNames(obj) [ objEnumTrue, objEnumFalse ]继承的影响 只有for-in循环参见上面的示例和in运算符考虑继承 toString in obj trueobj.hasOwnProperty(toString) falseobj.hasOwnProperty(objEnumFalse) true计算对象的自有属性数量 对象没有length或size这样的方法所以你必须使用以下的解决方法 Object.keys(obj).length最佳实践遍历自有属性 遍历属性键 结合for-in和hasOwnProperty()以for-in中描述的方式。这甚至可以在旧的 JavaScript 引擎上工作。例如 for (var key in obj) {if (Object.prototype.hasOwnProperty.call(obj, key)) {console.log(key);} }结合Object.keys()或Object.getOwnPropertyNames()与forEach()数组迭代 var obj { first: John, last: Doe }; // Visit non-inherited enumerable keys Object.keys(obj).forEach(function (key) {console.log(key); });遍历属性值或(key, value)对 遍历键并使用每个键检索相应的值。其他语言可能会更简单但 JavaScript 不会。 访问器Getters 和 Setters ECMAScript 5 允许你编写方法其调用看起来像是在获取或设置属性。这意味着属性是虚拟的而不是存储空间。例如你可以禁止设置属性并且总是在读取时计算返回的值。 通过对象字面量定义访问器 以下示例使用对象字面量为属性foo定义了一个 setter 和一个 getter var obj {get foo() {return getter;},set foo(value) {console.log(setter: value);} };以下是交互 obj.foo bla; setter: blaobj.foo getter通过属性描述符定义访问器 指定 getter 和 setter 的另一种方式是通过属性描述符参见属性描述符。以下代码定义了与前面的字面量相同的对象 var obj Object.create(Object.prototype, { // object with property descriptorsfoo: { // property descriptorget: function () {return getter;},set: function (value) {console.log(setter: value);}}} );访问器和继承 Getter 和 setter 是从原型继承的 var proto { get foo() { return hello } };var obj Object.create(proto); obj.foo hello属性属性和属性描述符 提示 属性属性和属性描述符是一个高级主题。通常你不需要知道它们是如何工作的。 在本节中我们将看一下属性的内部结构 属性属性是属性的原子构建块。 属性描述符是一个用于以编程方式处理属性的数据结构。 属性属性 属性的所有状态包括其数据和元数据都存储在属性中。它们是属性具有的字段就像对象具有属性一样。属性键通常用双括号写入。属性对于普通属性和访问器getter 和 setter都很重要。 以下属性是特定于普通属性的 [[Value]]保存属性的值它的数据。 [[Writable]]保存一个布尔值指示属性的值是否可以被更改。 以下属性是特定于访问器的 [[Get]]保存 getter当属性被读取时调用的函数。该函数计算读取访问的结果。 [[Set]]保存 setter当属性被设置为一个值时调用的函数。该函数将该值作为参数接收。 所有属性都具有以下属性 [[Enumerable]]保存一个布尔值。使属性不可枚举会隐藏它使其无法被某些操作检测到参见属性的迭代和检测。 [[可配置]]保存一个布尔值。如果它是false您不能删除属性更改其任何属性除了[[值]]或者将其从数据属性转换为访问器属性反之亦然。换句话说[[可配置]]控制属性元数据的可写性。有一个例外规则 - JavaScript 允许您将不可配置的属性从可写更改为只读出于历史原因数组的属性length一直是可写的且不可配置的。没有这个例外您将无法冻结参见冻结数组。 默认值 如果您不指定属性则使用以下默认值 属性键默认值[[值]]undefined[[获取]]undefined[[设置]]undefined[[可写]]false[[可枚举]]false[[可配置]]false 当您通过属性描述符创建属性时这些默认值非常重要请参阅下一节。 属性描述符 属性描述符是用于以编程方式处理属性的数据结构。它是一个编码属性的属性的对象。描述符的每个属性对应一个属性。例如以下是值为 123 的只读属性的描述符 {value: 123,writable: false,enumerable: true,configurable: false }您可以通过访问器实现相同的目标即不可变性。然后描述符如下所示 {get: function () { return 123 },enumerable: true,configurable: false }通过描述符获取和定义属性 属性描述符用于两种操作 获取属性 属性的所有属性都作为描述符返回。 定义属性 定义属性意味着根据属性是否已存在而有所不同 如果属性不存在则创建一个新属性其属性由描述符指定。如果描述符中没有相应的属性则使用默认值。默认值由属性名称的含义决定。它们与通过赋值创建属性时使用的值相反然后属性是可写的可枚举的和可配置的。例如 var obj {};Object.defineProperty(obj, foo, { configurable: true });Object.getOwnPropertyDescriptor(obj, foo) { value: undefined,writable: false,enumerable: false,configurable: true }我通常不依赖默认值并明确声明所有属性以确保完全清晰。 如果属性已经存在则根据描述符指定的属性更新属性的属性。如果描述符中没有相应的属性则不要更改它。这是一个例子从上一个例子继续 Object.defineProperty(obj, foo, { writable: true });Object.getOwnPropertyDescriptor(obj, foo) { value: undefined,writable: true,enumerable: false,configurable: true }以下操作允许您通过属性描述符获取和设置属性的属性 Object.getOwnPropertyDescriptor(obj, propKey) 返回obj的自有非继承的属性的描述符其键为propKey。如果没有这样的属性则返回undefined Object.getOwnPropertyDescriptor(Object.prototype, toString) { value: [Function: toString],writable: true,enumerable: false,configurable: true } Object.getOwnPropertyDescriptor({}, toString) undefinedObject.defineProperty(obj, propKey, propDesc) 创建或更改obj的属性其键为propKey其属性通过propDesc指定。返回修改后的对象。例如 var obj Object.defineProperty({}, foo, {value: 123,enumerable: true// writable: false (default value)// configurable: false (default value) });Object.defineProperties(obj, propDescObj) Object.defineProperty()的批量版本。propDescObj的每个属性都保存一个属性描述符。属性的键和它们的值告诉Object.defineProperties在obj上创建或更改哪些属性。例如 var obj Object.defineProperties({}, {foo: { value: 123, enumerable: true },bar: { value: abc, enumerable: true } });Object.create(proto, propDescObj?) 首先创建一个原型为proto的对象。然后如果已指定可选参数propDescObj则以与Object.defineProperties相同的方式向其添加属性。最后返回结果。例如以下代码片段产生与上一个片段相同的结果 var obj Object.create(Object.prototype, {foo: { value: 123, enumerable: true },bar: { value: abc, enumerable: true } });复制对象 要创建对象的相同副本您需要正确获取两件事 复制必须具有与原始对象相同的原型参见第 2 层对象之间的原型关系。 复制必须具有与原始对象相同的属性并且具有相同的属性。 以下函数执行这样的复制 function copyObject(orig) {// 1\. copy has same prototype as origvar copy Object.create(Object.getPrototypeOf(orig));// 2\. copy has all of orig’s propertiescopyOwnPropertiesFrom(copy, orig);return copy; }属性通过这个函数从orig复制到copy。 function copyOwnPropertiesFrom(target, source) {Object.getOwnPropertyNames(source) // (1).forEach(function(propKey) { // (2)var desc Object.getOwnPropertyDescriptor(source, propKey); // (3)Object.defineProperty(target, propKey, desc); // (4)});return target; };涉及的步骤如下 获取一个包含source的所有自有属性键的数组。 遍历这些键。 检索属性描述符。 使用该属性描述符在target中创建一个自有属性。 请注意这个函数与 Underscore.js 库中的函数_.extend()非常相似。 属性定义与赋值 以下两个操作非常相似 通过defineProperty()和defineProperties()参见通过描述符获取和定义属性定义属性。 通过对属性进行赋值。 然而有一些微妙的差异 定义属性意味着创建一个新的自有属性或更新现有自有属性的属性。在这两种情况下原型链完全被忽略。 对属性进行赋值 prop意味着改变现有属性。过程如下 如果prop是一个 setter自有或继承的调用该 setter。 否则如果prop是只读的自有或继承的抛出异常在严格模式下或不做任何操作在松散模式下。下一节将更详细地解释这个稍微意外的现象。 否则如果prop是自有的并且可写的改变该属性的值。 否则要么没有属性prop要么它是继承的并且可写的。在这两种情况下定义一个可写、可配置和可枚举的自有属性prop。在后一种情况下我们刚刚覆盖了一个继承的属性非破坏性地改变了它。在前一种情况下一个丢失的属性已经被自动定义。这种自动定义是有问题的因为在赋值中的拼写错误可能很难检测到。 继承的只读属性不能被赋值。 如果一个对象obj从原型继承了属性foo并且foo不可写那么你不能对obj.foo进行赋值 var proto Object.defineProperty({}, foo, {value: a,writable: false }); var obj Object.create(proto);obj从proto继承了只读属性foo。在松散模式下设置属性没有效果。 obj.foo b;obj.foo a在严格模式下会抛出异常 (function () { use strict; obj.foo b }()); TypeError: Cannot assign to read-only property foo这符合继承属性会改变但是非破坏性的想法。如果继承属性是只读的你希望禁止所有更改甚至是非破坏性的更改。 请注意您可以通过定义一个自有属性来规避此保护请参阅前一小节了解定义和赋值之间的区别 Object.defineProperty(obj, foo, { value: b });obj.foo b枚举性最佳实践 一般规则是系统创建的属性是不可枚举的而用户创建的属性是可枚举的 Object.keys([]) []Object.getOwnPropertyNames([]) [ length ] Object.keys([a]) [ 0 ]这对于内置实例原型的方法特别适用 Object.keys(Object.prototype) []Object.getOwnPropertyNames(Object.prototype) [ hasOwnProperty,valueOf,constructor,toLocaleString,isPrototypeOf,propertyIsEnumerable,toString ]枚举性的主要目的是告诉for-in循环它应该忽略哪些属性。正如我们刚才在查看内置构造函数的实例时所看到的用户未创建的所有内容都会被for-in隐藏。 受枚举性影响的唯一操作是 for-in循环 Object.keys()列出自有属性键 JSON.stringify()JSON.stringify(value, replacer?, space?) 以下是一些需要牢记的最佳实践 对于你自己的代码通常可以忽略枚举性并且应该避免使用for-in循环最佳实践遍历数组。 通常不应向内置原型和对象添加属性。但如果您这样做应该使它们不可枚举以避免破坏现有代码。 保护对象 保护对象有三个级别从弱到强依次列出 防止扩展 封印 冻结 防止扩展 通过以下方式防止扩展 Object.preventExtensions(obj)使向obj添加属性变得不可能。例如 var obj { foo: a }; Object.preventExtensions(obj);现在在松散模式下添加属性会悄悄失败 obj.bar b;obj.bar undefined并在严格模式下抛出错误 (function () { use strict; obj.bar b }()); TypeError: Cant add property bar, object is not extensible您仍然可以删除属性 delete obj.foo trueobj.foo undefined您可以通过以下方式检查对象是否可扩展 Object.isExtensible(obj)封印 通过以下方式封印 Object.seal(obj)防止扩展并使所有属性“不可配置”。后者意味着属性的属性参见属性属性和属性描述符不能再改变。例如只读属性将永远保持只读。 以下示例演示了封印使所有属性都不可配置 var obj { foo: a }; Object.getOwnPropertyDescriptor(obj, foo) // before sealing { value: a,writable: true,enumerable: true,configurable: true } Object.seal(obj) Object.getOwnPropertyDescriptor(obj, foo) // after sealing { value: a,writable: true,enumerable: true,configurable: false }你仍然可以改变属性foo obj.foo b; bobj.foo b但你不能改变它的属性 Object.defineProperty(obj, foo, { enumerable: false }); TypeError: Cannot redefine property: foo您可以通过以下方式检查对象是否被封闭 Object.isSealed(obj)冻结 通过以下方式进行冻结 Object.freeze(obj)它使所有属性都不可写并封闭obj。换句话说obj不可扩展所有属性都是只读的没有办法改变。让我们看一个例子 var point { x: 17, y: -5 }; Object.freeze(point);在松散模式下再次出现悄悄失败 point.x 2; // no effect, point.x is read-onlypoint.x 17 point.z 123; // no effect, point is not extensiblepoint { x: 17, y: -5 }在严格模式下会出现错误 (function () { use strict; point.x 2 }()); TypeError: Cannot assign to read-only property x (function () { use strict; point.z 123 }()); TypeError: Cant add property z, object is not extensible您可以通过以下方式检查对象是否被冻结 Object.isFrozen(obj)陷阱保护是浅层的 保护对象是浅层的它影响自有属性但不影响这些属性的值。例如考虑以下对象 var obj {foo: 1,bar: [a, b] }; Object.freeze(obj);即使您已经冻结了obj它并不是完全不可变的——您可以改变属性bar的可变值 obj.foo 2; // no effectobj.bar.push(c); // changes obj.bar obj { foo: 1, bar: [ a, b, c ] }此外obj具有原型Object.prototype它也是可变的。 第三层构造函数——实例的工厂 构造函数简称构造函数有助于生成某种相似的对象。它是一个普通函数但是命名、设置和调用方式都不同。本节解释了构造函数的工作原理。它们对应于其他语言中的类。 我们已经看到了两个相似的对象的例子在通过原型在对象之间共享数据中 var PersonProto {describe: function () {return Person named this.name;} }; var jane {[[Prototype]]: PersonProto,name: Jane }; var tarzan {[[Prototype]]: PersonProto,name: Tarzan };对象jane和tarzan都被认为是“人”并共享原型对象PersonProto。让我们将该原型转换为一个构造函数Person用于创建像jane和tarzan这样的对象。构造函数创建的对象称为它的实例。这样的实例与jane和tarzan具有相同的结构由两部分组成 数据是特定于实例的并存储在实例对象的自有属性中在前面的例子中是jane和tarzan。 所有实例共享行为——它们有一个共同的原型对象和方法在前面的例子中是PersonProto。 构造函数是通过new运算符调用的函数。按照惯例构造函数的名称以大写字母开头而普通函数和方法的名称以小写字母开头。函数本身设置了第一部分 function Person(name) {this.name name; }Person.prototype中的对象成为Person的所有实例的原型。它贡献了第二部分 Person.prototype.describe function () {return Person named this.name; };让我们创建并使用Person的一个实例 var jane new Person(Jane);jane.describe() Person named Jane我们可以看到Person是一个普通函数。只有当通过new调用它时它才成为构造函数。new运算符执行以下步骤 首先设置行为创建一个新对象其原型是Person.prototype。 然后数据设置完成Person接收该对象作为隐式参数this并添加实例属性。 图 17-3 展示了实例jane的样子。Person.prototype的constructor属性指向构造函数并在实例的构造函数属性中有解释。 图 17-3 jane 是构造函数 Person 的一个实例它的原型是对象 Person.prototype。 instanceof运算符允许我们检查一个对象是否是给定构造函数的实例 jane instanceof Person truejane instanceof Date falseJavaScript 中实现的new运算符 如果你手动实现new运算符它看起来大致如下 function newOperator(Constr, args) {var thisValue Object.create(Constr.prototype); // (1)var result Constr.apply(thisValue, args);if (typeof result object result ! null) {return result; // (2)}return thisValue; }在第1行你可以看到由构造函数Constr创建的实例的原型是Constr.prototype。 第2行揭示了new运算符的另一个特性你可以从构造函数中返回任意对象并且它将成为new运算符的结果。如果你希望构造函数返回一个子构造函数的实例这是很有用的一个例子在从构造函数返回任意对象中给出。 术语两个原型 不幸的是在 JavaScript 中术语prototype被使用得含糊不清 原型 1原型关系 一个对象可以是另一个对象的原型 var proto {};var obj Object.create(proto);Object.getPrototypeOf(obj) proto true在前面的例子中proto是obj的原型。 原型 2属性prototype的值 每个构造函数C都有一个指向对象的prototype属性。这个对象成为C的所有实例的原型 function C() {}Object.getPrototypeOf(new C()) C.prototype true通常上下文会清楚表明是指两个原型中的哪一个。如果需要消除歧义那么我们就需要使用prototype来描述对象之间的关系因为这个名称已经通过getPrototypeOf和isPrototypeOf进入了标准库。因此我们需要为prototype属性引用的对象找到一个不同的名称。一个可能的选择是constructor prototype但这是有问题的因为构造函数也有原型 function Foo() {}Object.getPrototypeOf(Foo) Function.prototype true因此instance prototype是最佳选择。 实例的构造函数属性 默认情况下每个函数C都包含一个实例原型对象C.prototype它的constructor属性指向C function C() {}C.prototype.constructor C true因为每个实例都从原型继承了constructor属性所以你可以使用它来获取实例的构造函数 var o new C();o.constructor [Function: C]构造函数属性的用例 切换对象的构造函数 在下面的catch子句中我们根据捕获的异常的构造函数采取不同的操作 try {... } catch (e) {switch (e.constructor) {case SyntaxError:...break;case CustomError:...break;...} }警告 这种方法只能检测给定构造函数的直接实例。相比之下instanceof可以检测直接实例和所有子构造函数的实例。 确定对象的构造函数名称 例如 function Foo() {}var f new Foo();f.constructor.name Foo警告 并非所有的 JavaScript 引擎都支持函数的name属性。 创建类似的对象 这是如何创建一个新对象y它具有与现有对象x相同的构造函数 function Constr() {} var x new Constr();var y new x.constructor(); console.log(y instanceof Constr); // true这个技巧对于一个必须适用于子构造函数的实例并且想要创建一个类似于this的新实例的方法非常有用。然后你就不能使用一个固定的构造函数 SuperConstr.prototype.createCopy function () {return new this.constructor(...); };引用超级构造函数 一些继承库将超级原型分配给子构造函数的一个属性。例如YUI 框架通过Y.extend提供子类化 function Super() { } function Sub() {Sub.superclass.constructor.call(this); // (1) } Y.extend(Sub, Super);在第1行的调用有效因为extend将Sub.superclass设置为Super.prototype。由于constructor属性你可以将超级构造函数作为方法调用。 注意 instanceof运算符参见The instanceof Operator不依赖于constructor属性。 最佳实践 确保对于每个构造函数C以下断言成立 C.prototype.constructor C默认情况下每个函数f已经有一个正确设置的属性prototype function f() {}f.prototype.constructor f true因此你应该避免替换这个对象只向它添加属性 // Avoid: C.prototype {method1: function (...) { ... },... };// Prefer: C.prototype.method1 function (...) { ... }; ...如果你替换它你应该手动将正确的值赋给constructor C.prototype {constructor: C,method1: function (...) { ... },... };请注意JavaScript 中没有任何关键的东西取决于constructor属性但是设置它是一个好的风格因为它可以启用本节中提到的技术。 instanceof 运算符 instanceof运算符 value instanceof Constr通过检查Constr.prototype是否在value的原型链中确定value是由构造函数Constr或子构造函数创建的。因此以下两个表达式是等价的 value instanceof Constr Constr.prototype.isPrototypeOf(value)以下是一些例子 {} instanceof Object true [] instanceof Array // constructor of [] true[] instanceof Object // super-constructor of [] true new Date() instanceof Date truenew Date() instanceof Object true预期的是对于原始值instanceof总是false abc instanceof Object false123 instanceof Number false最后如果它的右侧不是一个函数instanceof会抛出一个异常 [] instanceof 123 TypeError: Expecting a function in instanceof check陷阱不是Object的实例的对象 几乎所有的对象都是Object的实例因为它们的原型链中有Object.prototype。但也有一些对象不是这样。以下是两个例子 Object.create(null) instanceof Object falseObject.prototype instanceof Object false前一个对象在The dict Pattern: Objects Without Prototypes Are Better Maps中有更详细的解释。后一个对象是大多数原型链的终点它们必须在某个地方结束。两个对象都没有原型 Object.getPrototypeOf(Object.create(null)) nullObject.getPrototypeOf(Object.prototype) null但是typeof正确地将它们分类为对象 typeof Object.create(null) objecttypeof Object.prototype object对于instanceof的大多数用例来说这个陷阱并不是一个断点但你必须意识到它。 陷阱跨越领域框架或窗口 在 Web 浏览器中每个框架和窗口都有自己的领域具有单独的全局变量。这可以防止instanceof对跨越领域的对象起作用。要了解原因请看下面的代码 if (myvar instanceof Array) ... // Doesn’t always work如果myvar是来自不同领域的数组那么它的原型是该领域的Array.prototype。因此instanceof不会在myvar的原型链中找到当前领域的Array.prototype并且会返回false。ECMAScript 5 有一个函数Array.isArray()它总是有效的 headscriptfunction test(arr) {var iframe frames[0];console.log(arr instanceof Array); // falseconsole.log(arr instanceof iframe.Array); // trueconsole.log(Array.isArray(arr)); // true}/script /head bodyiframe srcdocscriptwindow.parent.test([])/script/iframe /body显然这也是非内置构造函数的问题。 除了使用Array.isArray()还有几件事情可以解决这个问题 避免对象跨越领域。浏览器有postMessage()方法可以将一个对象复制到另一个领域而不是传递一个引用。 检查实例的构造函数的名称仅适用于支持函数name属性的引擎 someValue.constructor.name NameOfExpectedConstructor使用原型属性标记实例属于类型T。有几种方法可以这样做。检查value是否是T的实例如下 value.isT(): T实例的原型必须从这个方法返回true一个常见的超级构造函数应该返回默认值false。 T in value: 你必须用一个属性标记T实例的原型其键是T或者更独特的东西。 value.TYPE_NAME T: 每个相关的原型必须有一个TYPE_NAME属性具有适当的值。 实现构造函数的提示 本节提供了一些实现构造函数的提示。 防止忘记新的严格模式 如果你在使用构造函数时忘记了new你是将它作为函数而不是构造函数来调用。在松散模式下你不会得到一个实例全局变量会被创建。不幸的是所有这些都是没有警告发生的 function SloppyColor(name) {this.name name; } var c SloppyColor(green); // no warning!// No instance is created: console.log(c); // undefined // A global variable is created: console.log(name); // green在严格模式下你会得到一个异常 function StrictColor(name) {use strict;this.name name; } var c StrictColor(green); // TypeError: Cannot set property name of undefined从构造函数返回任意对象 在许多面向对象的语言中构造函数只能生成直接实例。例如考虑 Java假设您想要实现一个类Expression它有子类Addition和Multiplication。解析会生成后两个类的直接实例。您不能将其实现为Expression的构造函数因为该构造函数只能生成Expression的直接实例。作为解决方法在 Java 中使用静态工厂方法 class Expression {// Static factory method:public static Expression parse(String str) {if (...) {return new Addition(...);} else if (...) {return new Multiplication(...);} else {throw new ExpressionException(...);}} } ... Expression expr Expression.parse(someStr);在 JavaScript 中您可以从构造函数中简单地返回您需要的任何对象。因此前面代码的 JavaScript 版本看起来像 function Expression(str) {if (...) {return new Addition(..);} else if (...) {return new Multiplication(...);} else {throw new ExpressionException(...);} } ... var expr new Expression(someStr);这是个好消息JavaScript 构造函数不会将你锁定因此您可以随时改变构造函数是否应返回直接实例或其他内容的想法。 原型属性中的数据 本节解释了在大多数情况下您不应该将数据放在原型属性中。然而这个规则也有一些例外。 避免具有实例属性初始值的原型属性 原型包含多个对象共享的属性。因此它们非常适用于方法。此外通过下面描述的一种技术您还可以使用它们来为实例属性提供初始值。稍后我会解释为什么不建议这样做。 构造函数通常将实例属性设置为初始值。如果其中一个值是默认值那么您不需要创建实例属性。您只需要一个具有相同键的原型属性其值是默认值。例如 /*** Anti-pattern: don’t do this** param data an array with names*/ function Names(data) {if (data) {// There is a parameter// create instance propertythis.data data;} } Names.prototype.data [];参数data是可选的。如果缺少它实例将不会获得属性data而是继承Names.prototype.data。 这种方法基本上有效您可以创建Names的实例n。获取n.data会读取Names.prototype.data。设置n.data会在n中创建一个新的自有属性并保留原型中的共享默认值。我们只有一个问题如果我们更改默认值而不是用新值替换它 var n1 new Names();var n2 new Names(); n1.data.push(jane); // changes default valuen1.data [ jane ] n2.data [ jane ]在前面的示例中push()改变了Names.prototype.data中的数组。由于该数组被所有没有自有属性data的实例共享因此n2.data的初始值也发生了变化。 最佳实践不要共享默认值 鉴于我们刚刚讨论的内容最好不要共享默认值并且始终创建新的默认值 function Names(data) {this.data data || []; }显然如果该值是不可变的就像所有原始值一样请参阅Primitive Values那么修改共享默认值的问题就不会出现。但为了保持一致性最好坚持一种设置属性的方式。我也更喜欢保持通常的关注点分离参见Layer 3: Constructors—Factories for Instances构造函数设置实例属性原型包含方法。 ECMAScript 6 将使这更加成为最佳实践因为构造函数参数可以具有默认值并且您可以通过类定义原型方法但不能定义具有数据的原型属性。 按需创建实例属性 偶尔创建属性值是一个昂贵的操作在计算或存储方面。在这种情况下您可以按需创建实例属性 function Names(data) {if (data) this.data data; } Names.prototype {constructor: Names, // (1)get data() {// Define, don’t assign// avoid calling the (nonexistent) setterObject.defineProperty(this, data, {value: [],enumerable: true,configurable: false,writable: false});return this.data;} };我们无法通过赋值向实例添加属性data因为 JavaScript 会抱怨缺少 setter当它只找到 getter 时会这样做。因此我们通过Object.defineProperty()来添加它。请参阅Properties: Definition Versus Assignment来查看定义和赋值之间的区别。在第1行我们确保属性constructor被正确设置参见The constructor Property of Instances。 显然这是相当多的工作所以你必须确保它是值得的。 避免非多态原型属性 如果相同的属性相同的键相同的语义通常不同的值存在于几个原型中则称为多态。然后通过实例读取属性的结果是通过该实例的原型动态确定的。未多态使用的原型属性可以被变量替换这更好地反映了它们的非多态性质。 例如你可以将常量存储在原型属性中并通过this访问它 function Foo() {} Foo.prototype.FACTOR 42; Foo.prototype.compute function (x) {return x * this.FACTOR; };这个常量不是多态的。因此你可以通过变量访问它 // This code should be inside an IIFE or a module function Foo() {} var FACTOR 42; Foo.prototype.compute function (x) {return x * FACTOR; };多态原型属性 这是一个具有不可变数据的多态原型属性的示例。通过原型属性标记构造函数的实例可以将它们与不同构造函数的实例区分开来 function ConstrA() { } ConstrA.prototype.TYPE_NAME ConstrA;function ConstrB() { } ConstrB.prototype.TYPE_NAME ConstrB;由于多态的“标签”TYPE_NAME即使它们跨越领域然后instanceof不起作用参见陷阱跨领域帧或窗口你也可以区分ConstrA和ConstrB的实例。 保持数据私有 JavaScript 没有专门的手段来管理对象的私有数据。本节将描述三种解决这个限制的技术 构造函数环境中的私有数据 使用标记键在属性中存储私有数据 使用具体键在属性中存储私有数据 此外我将解释如何通过 IIFE 保持全局数据私有。 构造函数环境中的私有数据Crockford 隐私模式 当调用构造函数时会创建两个东西构造函数的实例和一个环境参见环境管理变量。实例由构造函数初始化。环境保存构造函数的参数和局部变量。在构造函数内部创建的每个函数包括方法都将保留对环境的引用——它被创建的环境。由于这个引用即使构造函数完成后它仍然可以访问环境。这种函数和环境的组合被称为闭包闭包函数保持与它们的诞生作用域连接。构造函数的环境因此是独立于实例的数据存储与实例只有因为它们同时创建而相关。为了正确连接它们我们必须有生活在两个世界中的函数。使用Douglas Crockford 的术语一个实例可以有三种与之关联的值参见图 17-4 公共属性 存储在属性中的值无论是在实例中还是在其原型中都是公开可访问的。 私有值 存储在环境中的数据和函数是私有的——只能由构造函数和它创建的函数访问。 特权方法 私有函数可以访问公共属性但原型中的公共方法无法访问私有数据。因此我们需要特权方法——实例中的公共方法。特权方法是公共的可以被所有人调用但它们也可以访问私有值因为它们是在构造函数中创建的。 图 17-4当构造函数 Constr 被调用时会创建两个数据结构参数和局部变量的环境以及要初始化的实例。 以下各节详细解释了每种值。 公共属性 请记住对于构造函数Constr有两种公共属性可供所有人访问。首先原型属性存储在Constr.prototype中并由所有实例共享。原型属性通常是方法 Constr.prototype.publicMethod ...;其次实例属性对每个实例都是唯一的。它们在构造函数中添加通常保存数据而不是方法 function Constr(...) {this.publicData ...;... }私有值 构造函数的环境包括参数和局部变量。它们只能从构造函数内部访问因此对实例是私有的 function Constr(...) {...var that this; // make accessible to private functionsvar privateData ...;function privateFunction(...) {// Access everythingprivateData ...;that.publicData ...;that.publicMethod(...);}... }特权方法 私有数据是如此安全以至于原型方法无法访问它。但是离开构造函数后你还能怎么使用它呢答案是特权方法在构造函数中创建的函数被添加为实例方法。这意味着一方面它们可以访问私有数据另一方面它们是公共的因此被原型方法看到。换句话说它们在私有数据和公共数据包括原型方法之间充当中介 function Constr(...) {...this.privilegedMethod function (...) {// Access everythingprivateData ...;privateFunction(...);this.publicData ...;this.publicMethod(...);}; }一个例子 以下是使用 Crockford 隐私模式实现的StringBuilder function StringBuilder() {var buffer [];this.add function (str) {buffer.push(str);};this.toString function () {return buffer.join();}; } // Can’t put methods in the prototype!以下是交互 var sb new StringBuilder();sb.add(Hello);sb.add( world!);sb.toString() ’Hello world!’Crockford 隐私模式的利弊 在使用 Crockford 隐私模式时需要考虑的一些要点 它不是很优雅 通过特权方法介入私有数据的访问引入了不必要的间接性。特权方法和私有函数都破坏了构造函数设置实例数据和实例原型方法之间的关注点分离。 它是完全安全的 无法从外部访问环境的数据这使得这种解决方案在需要时非常安全例如对于安全关键代码。另一方面私有数据不可被外部访问也可能会带来不便。有时你想对私有功能进行单元测试。而一些临时的快速修复依赖于访问私有数据的能力。这种快速修复是无法预测的所以无论你的设计有多好都可能会出现这种需求。 它可能会更慢 在当前 JavaScript 引擎中访问原型链中的属性是高度优化的。访问闭包中的值可能会更慢。但这些事情不断变化所以你必须测量一下看看这对你的代码是否真的很重要。 它会消耗更多的内存 保留环境并将特权方法放在实例中会消耗内存。再次确保这对你的代码真的很重要并进行测量。 带有标记键的属性中的私有数据 对于大多数非安全关键的应用程序来说隐私更像是 API 的一个提示“你不需要看到这个。”这就是封装的关键好处——隐藏复杂性。尽管底层可能有更多的东西但你只需要理解 API 的公共部分。命名约定的想法是通过标记属性的键来让客户端了解隐私。通常会使用前缀下划线来实现这一目的。 让我们重写先前的StringBuilder示例以便缓冲区保存在名为_buffer的私有属性中但按照惯例而言 function StringBuilder() {this._buffer []; } StringBuilder.prototype {constructor: StringBuilder,add: function (str) {this._buffer.push(str);},toString: function () {return this._buffer.join();} };以下是通过标记属性键实现隐私的一些利弊 它提供了更自然的编码风格 能够以相同的方式访问私有和公共数据比使用环境实现隐私更加优雅。 它污染了属性的命名空间 具有标记键的属性可以在任何地方看到。人们使用 IDE 的越多它们就会越烦人因为它们会显示在公共属性旁边而应该隐藏在那里。理论上IDE 可以通过识别命名约定并在可能的情况下隐藏私有属性来进行适应。 可以从“外部”访问私有属性 这对单元测试和快速修复很有用。此外子构造函数和辅助函数所谓的“友元函数”可以更轻松地访问私有数据。环境方法不提供这种灵活性私有数据只能从构造函数内部访问。 它可能导致关键冲突 私有属性的键可能会发生冲突。这已经是子构造函数的一个问题但如果您使用多重继承某些库允许的这将更加棘手。通过环境方法就不会发生任何冲突。 使用具体化键在属性中保持私有数据 私有属性的一个问题是键可能会发生冲突例如来自构造函数的键与来自子构造函数的键或来自混入的键与来自构造函数的键。通过使用更长的键例如包含构造函数名称的键可以减少这种冲突的可能性。然后在前面的情况下私有属性_buffer将被称为_StringBuilder_buffer。如果这样的键对您来说太长您可以选择具体化它将其存储在变量中 var KEY_BUFFER _StringBuilder_buffer;现在我们通过this[KEY_BUFFER]访问私有数据。 var StringBuilder function () {var KEY_BUFFER _StringBuilder_buffer;function StringBuilder() {this[KEY_BUFFER] [];}StringBuilder.prototype {constructor: StringBuilder,add: function (str) {this[KEY_BUFFER].push(str);},toString: function () {return this[KEY_BUFFER].join();}};return StringBuilder; }();我们已经将 IIFE 包装在StringBuilder周围以便常量KEY_BUFFER保持本地化不会污染全局命名空间。 具体化的属性键使您能够在键中使用 UUID通用唯一标识符。例如通过 Robert Kieffer 的node-uuid var KEY_BUFFER _StringBuilder_buffer_ uuid.v4();每次代码运行时KEY_BUFFER的值都不同。例如可能如下所示 _StringBuilder_buffer_110ec58a-a0f2-4ac4-8393-c866d813b8d1具有 UUID 的长键使关键冲突几乎不可能发生。 通过 IIFE 将全局数据保持私有 本小节解释了如何通过 IIFE请参阅通过 IIFE 引入新作用域将全局数据保持私有以供单例对象、构造函数和方法使用。这些 IIFE 创建新环境请参阅环境管理变量您可以在其中放置私有数据。 将私有全局数据附加到单例对象 您不需要构造函数来将对象与环境中的私有数据关联起来。以下示例显示了如何使用 IIFE 来实现相同的目的方法是将其包装在单例对象周围 var obj function () { // open IIFE// publicvar self {publicMethod: function (...) {privateData ...;privateFunction(...);},publicData: ...};// privatevar privateData ...;function privateFunction(...) {privateData ...;self.publicData ...;self.publicMethod(...);}return self; }(); // close IIFE将全局数据保持私有以供所有构造函数使用 某些全局数据仅适用于构造函数和原型方法。通过同时将 IIFE 包装在两者周围可以将其隐藏起来不让公众看到。使用具体化键在属性中保持私有数据举例说明构造函数StringBuilder及其原型方法使用常量KEY_BUFFER其中包含属性键。该常量存储在 IIFE 的环境中 var StringBuilder function () { // open IIFEvar KEY_BUFFER _StringBuilder_buffer_ uuid.v4();function StringBuilder() {this[KEY_BUFFER] [];}StringBuilder.prototype {// Omitted: methods accessing this[KEY_BUFFER]};return StringBuilder; }(); // close IIFE请注意如果您使用模块系统请参阅第三十一章您可以通过将构造函数加上方法放入模块中以更干净的代码实现相同的效果。 将全局数据附加到方法 有时您只需要单个方法的全局数据。通过将其放在您包装在方法周围的 IIFE 的环境中可以使其保持私有。例如 var obj {method: function () { // open IIFE// method-private datavar invocCount 0;return function () {invocCount;console.log(Invocation #invocCount);return result;};}() // close IIFE };以下是交互 obj.method() Invocation #1 resultobj.method() Invocation #2 result第 4 层构造函数之间的继承 在本节中我们将研究如何从构造函数中继承给定一个构造函数Super我们如何编写一个新的构造函数Sub它具有Super的所有特性以及一些自己的特性不幸的是JavaScript 没有内置的机制来执行这个任务。因此我们需要做一些手动工作。 图 17-5 说明了这个想法子构造函数Sub应该具有Super的所有属性原型属性和实例属性另外还有自己的。因此我们对Sub应该是什么样子有了一个大致的想法但不知道如何实现。我们需要弄清楚几件事情接下来我会解释 继承实例属性。 继承原型属性。 确保instanceof的工作如果sub是Sub的一个实例我们也希望sub instanceof Super为真。 覆盖方法以适应Sub中的Super方法之一。 进行超级调用如果我们覆盖了Super的一个方法我们可能需要从Sub中调用原始方法。 图 17-5Sub 应该从 Super 继承它应该具有 Super 的所有原型属性和所有 Super 的实例属性另外还有自己的。请注意methodB 覆盖了 Super 的 methodB。 继承实例属性 实例属性是在构造函数本身中设置的因此继承超级构造函数的实例属性涉及调用该构造函数 function Sub(prop1, prop2, prop3, prop4) {Super.call(this, prop1, prop2); // (1)this.prop3 prop3; // (2)this.prop4 prop4; // (3) }当通过new调用Sub时它的隐式参数this指向一个新实例。它首先将该实例传递给Super1后者添加其实例属性。之后Sub设置自己的实例属性2,3。关键是不要通过new调用Super因为那样会创建一个新的超级实例。相反我们将Super作为一个函数调用并将当前子实例作为this的值传递进去。 继承原型属性 诸如方法之类的共享属性保存在实例原型中。因此我们需要找到一种方法让Sub.prototype继承Super.prototype的所有属性。解决方案是将Sub.prototype设置为Super.prototype的原型。 对两种原型感到困惑吗 是的JavaScript 术语在这里很令人困惑。如果你感到迷茫请参阅术语两个原型其中解释了它们的区别。 这是实现这一点的代码 Sub.prototype Object.create(Super.prototype); Sub.prototype.constructor Sub; Sub.prototype.methodB ...; Sub.prototype.methodC ...;Object.create()生成一个原型为Super.prototype的新对象。之后我们添加Sub的方法。正如在实例的构造函数属性中解释的那样我们还需要设置constructor属性因为我们已经替换了原始实例原型其中它具有正确的值。 图 17-6 显示了现在Sub和Super的关系。Sub的结构确实类似于我在图 17-5 中所勾画的。该图未显示实例属性这些属性是由图中提到的函数调用设置的。 图 17-6构造函数 Sub 通过调用构造函数 Super 并使 Sub.prototype 成为 Super.prototype 的原型而继承了构造函数 Super。 确保instanceof的工作 “确保instanceof的工作”意味着Sub的每个实例也必须是Super的实例。图 17-7 显示了Sub的实例subInstance的原型链的样子它的第一个原型是Sub.prototype第二个原型是Super.prototype。 图 17-7subInstance 是由构造函数 Sub 创建的。它具有两个原型 Sub.prototype 和 Super.prototype。 让我们从一个更简单的问题开始subInstance是Sub的一个实例吗是的因为以下两个断言是等价的后者可以被视为前者的定义 subInstance instanceof Sub Sub.prototype.isPrototypeOf(subInstance)如前所述Sub.prototype是subInstance的原型之一因此两个断言都为真。同样subInstance也是Super的一个实例因为以下两个断言成立 subInstance instanceof Super Super.prototype.isPrototypeOf(subInstance)重写一个方法 我们通过在Sub.prototype中添加与相同名称的方法来重写Super.prototype中的方法。methodB就是一个例子在图 17-7 中我们可以看到它为什么有效对methodB的搜索始于subInstance在找到Super.prototype.methodB之前找到了Sub.prototype.methodB。 进行超级调用 要理解超级调用您需要了解术语主对象。方法的主对象是拥有其值为方法的属性的对象。例如Sub.prototype.methodB的主对象是Sub.prototype。超级调用方法foo涉及三个步骤 从当前方法的主对象的原型“之后”在原型中开始搜索。 查找一个名为foo的方法。 使用当前的this调用该方法。其理由是超级方法必须与当前方法使用相同的实例它必须能够访问相同的实例属性。 因此子方法的代码如下所示。它超级调用自己调用它已经重写的方法 Sub.prototype.methodB function (x, y) {var superResult Super.prototype.methodB.call(this, x, y); // (1)return this.prop3 superResult; }阅读1处的超级调用的一种方式是直接引用超级方法并使用当前的this调用它。但是如果我们将其分为三个部分我们会发现上述步骤 Super.prototype从Super.prototype开始搜索即Sub.prototype的原型当前方法Sub.prototype.methodB的主对象。 methodB查找一个名为methodB的方法。 call(this, ...)调用在上一步中找到的方法并保持当前的this。 避免硬编码超级构造函数的名称 到目前为止我们总是通过提及超级构造函数名称来引用超级方法和超级构造函数。这种硬编码使您的代码不够灵活。您可以通过将超级原型分配给Sub的属性来避免这种情况 Sub._super Super.prototype;然后调用超级构造函数和超级方法如下所示 function Sub(prop1, prop2, prop3, prop4) {Sub._super.constructor.call(this, prop1, prop2);this.prop3 prop3;this.prop4 prop4; } Sub.prototype.methodB function (x, y) {var superResult Sub._super.methodB.call(this, x, y);return this.prop3 superResult; }设置Sub._super通常由一个实用函数处理该函数还将子原型连接到超级原型。例如 function subclasses(SubC, SuperC) {var subProto Object.create(SuperC.prototype);// Save constructor and, possibly, other methodscopyOwnPropertiesFrom(subProto, SubC.prototype);SubC.prototype subProto;SubC._super SuperC.prototype; };此代码使用了辅助函数copyOwnPropertiesFrom()该函数在复制对象中显示并解释。 提示 将“子类”解释为一个动词SubC 子类 SuperC。这样一个实用函数可以减轻创建子构造函数的痛苦手动操作的事情更少而且不会多次提及超级构造函数的名称。以下示例演示了它如何简化代码。 示例使用中的构造函数继承 具体示例假设构造函数Person已经存在 function Person(name) {this.name name; } Person.prototype.describe function () {return Person called this.name; };我们现在想要创建构造函数Employee作为Person的子构造函数。我们手动这样做看起来像这样 function Employee(name, title) {Person.call(this, name);this.title title; } Employee.prototype Object.create(Person.prototype); Employee.prototype.constructor Employee; Employee.prototype.describe function () {return Person.prototype.describe.call(this) (this.title); };以下是交互 var jane new Employee(Jane, CTO);jane.describe() Person called Jane (CTO)jane instanceof Employee truejane instanceof Person true前一节的实用函数subclasses()使Employee的代码稍微简化并避免了硬编码超级构造函数Person function Employee(name, title) {Employee._super.constructor.call(this, name);this.title title; } Employee.prototype.describe function () {return Employee._super.describe.call(this) (this.title); }; subclasses(Employee, Person);示例内置构造函数的继承层次结构 内置构造函数使用本节描述的相同子类化方法。例如Array是Object的子构造函数。因此Array的实例的原型链如下所示 var p Object.getPrototypeOf p([]) Array.prototype truep(p([])) Object.prototype truep(p(p([]))) null true反模式原型是超级构造函数的实例 在 ECMAScript 5 和Object.create()之前经常使用的解决方案是通过调用超级构造函数来创建子原型 Sub.prototype new Super(); // Don’t do this在 ECMAScript 5 下不推荐这样做。原型将具有所有Super的实例属性而它没有用处。因此最好使用上述模式涉及Object.create()。 所有对象的方法 几乎所有对象的原型链中都有Object.prototype Object.prototype.isPrototypeOf({}) trueObject.prototype.isPrototypeOf([]) trueObject.prototype.isPrototypeOf(/xyz/) true以下各小节描述了Object.prototype为其原型提供的方法。 转换为原始值 以下两种方法用于将对象转换为原始值 Object.prototype.toString() 返回对象的字符串表示 ({ first: John, last: Doe }.toString()) [object Object][ a, b, c ].toString() a,b,cObject.prototype.valueOf() 这是将对象转换为数字的首选方法。默认实现返回this var obj {};obj.valueOf() obj truevalueOf被包装构造函数覆盖以返回包装的原始值 new Number(7).valueOf() 7数字和字符串的转换无论是隐式还是显式都建立在原始值的转换基础上有关详细信息请参见算法ToPrimitive()—将值转换为原始值。这就是为什么您可以使用上述两种方法来配置这些转换。valueOf() 是数字转换的首选方法 3 * { valueOf: function () { return 5 } } 15toString() 是首选的字符串转换方法 String({ toString: function () { return ME } }) Result: ME布尔转换不可配置对象始终被视为true参见转换为布尔值。 Object.prototype.toLocaleString() 此方法返回对象的区域特定的字符串表示。默认实现调用toString()。大多数引擎对于此方法的支持不会超出此范围。然而ECMAScript 国际化 API参见ECMAScript 国际化 API由许多现代引擎支持它为几个内置构造函数覆盖了此方法。 原型继承和属性 以下方法有助于原型继承和属性 Object.prototype.isPrototypeOf(obj) 如果接收者是obj的原型链的一部分则返回true var proto { };var obj Object.create(proto);proto.isPrototypeOf(obj) trueobj.isPrototypeOf(obj) falseObject.prototype.hasOwnProperty(key) 如果this拥有一个键为key的属性则返回true。“拥有”意味着属性存在于对象本身而不是其原型链中的一个。 警告 通常应该通用地调用此方法而不是直接调用特别是在静态不知道属性的对象上。为什么以及如何在迭代和检测属性中有解释 var proto { foo: abc };var obj Object.create(proto);obj.bar def; Object.prototype.hasOwnProperty.call(obj, foo) falseObject.prototype.hasOwnProperty.call(obj, bar) trueObject.prototype.propertyIsEnumerable(propKey) 如果接收者具有具有可枚举键propKey的属性则返回true否则返回false var obj { foo: abc };obj.propertyIsEnumerable(foo) trueobj.propertyIsEnumerable(toString) falseobj.propertyIsEnumerable(unknown) false通用方法从原型中借用方法 有时实例原型具有对更多对象有用的方法而不仅仅是继承自它们的对象。本节解释了如何使用原型的方法而不继承它。例如实例原型Wine.prototype具有方法incAge() function Wine(age) {this.age age; } Wine.prototype.incAge function (years) {this.age years; }交互如下 var chablis new Wine(3);chablis.incAge(1);chablis.age 4incAge()方法适用于具有age属性的任何对象。我们如何在不是Wine实例的对象上调用它让我们看看前面的方法调用 chablis.incAge(1)实际上有两个参数 chablis是方法调用的接收器通过this传递给incAge。 1是一个参数通过years传递给incAge。 我们不能用任意对象替换前者——接收器必须是Wine的实例。否则找不到方法incAge。但前面的方法调用等同于参见Calling Functions While Setting this: call(), apply(), and bind() Wine.prototype.incAge.call(chablis, 1)通过前面的模式我们可以使一个对象成为接收器call的第一个参数而不是Wine的实例因为接收器不用于查找方法Wine.prototype.incAge。在下面的例子中我们将方法incAge()应用于对象john var john { age: 51 };Wine.prototype.incAge.call(john, 3)john.age 54可以以这种方式使用的函数称为通用方法它必须准备好this不是“它”的构造函数的实例。因此并非所有方法都是通用的ECMAScript 语言规范明确规定了哪些方法是通用的参见A List of All Generic Methods。 通过文字直接访问 Object.prototype 和 Array.prototype 通用调用方法相当冗长 Object.prototype.hasOwnProperty.call(obj, propKey)您可以通过访问空对象文字创建的 Object 实例来更简洁地访问hasOwnProperty {}.hasOwnProperty.call(obj, propKey)同样以下两个表达式是等价的 Array.prototype.join.call(str, -) [].join.call(str, -)这种模式的优势在于它不太啰嗦。但它也不太容易理解。性能不应该是一个问题至少从长远来看因为引擎可以静态确定文字不应该创建对象。 通用调用方法的示例 以下是一些通用方法的使用示例 使用apply()参见Function.prototype.apply(thisValue, argArray)来推送一个数组而不是单个元素参见Adding and Removing Elements (Destructive) var arr1 [ a, b ];var arr2 [ c, d ]; [].push.apply(arr1, arr2) 4arr1 [ a, b, c, d ]这个例子是关于将数组转换为参数而不是从另一个构造函数中借用方法。 将数组方法join()应用于字符串不是数组 Array.prototype.join.call(abc, -) a-b-c将数组方法map()应用于字符串:¹⁵ [].map.call(abc, function (x) { return x.toUpperCase() }) [ A, B, C ]通用地使用map()比使用split()更有效后者会创建一个中间数组 abc.split().map(function (x) { return x.toUpperCase() })[ A, B, C ] 将字符串方法应用于非字符串。toUpperCase()将接收器转换为字符串并将结果大写js String.prototype.toUpperCase.call(true)TRUE String.prototype.toUpperCase.call([a,b,c])A,B,C在普通对象上使用通用数组方法可以让您了解它们的工作原理 在伪数组上调用数组方法js var fakeArray { 0: a, 1: b, length: 2 }; Array.prototype.join.call(fakeArray, -)a-b 看看数组方法如何转换一个被视为数组的对象js var obj {}; Array.prototype.push.call(obj, hello);1 obj{ 0: hello, length: 1 }### 类似数组的对象和通用方法JavaScript 中有一些感觉像数组但实际上不是的对象。这意味着它们具有索引访问和length属性但它们没有任何数组方法forEach()pushconcat()等。这很不幸但正如我们将看到的通用数组方法可以实现一种解决方法。类似数组的对象的示例包括 特殊变量arguments参见[All Parameters by Index: The Special Variable arguments](ch15.html#arguments_variable All Parameters by Index: The Special Variable arguments)它是一个重要的类数组对象因为它是 JavaScript 的一个基本部分。arguments看起来像一个数组js function args() { return arguments } var arrayLike args(a, b); arrayLike[0]a arrayLike.length2但是没有任何数组方法可用js arrayLike.join(-)TypeError: object has no method join这是因为arrayLike不是Array的实例并且Array.prototype不在原型链中js arrayLike instanceof Arrayfalse 浏览器 DOM 节点列表由document.getElementsBy*()例如getElementsByTagName()、document.forms等返回js var elts document.getElementsByTagName(h3); elts.length3 elts instanceof Arrayfalse 字符串也是类数组的js abc[1]b abc.length3术语*类数组*也可以被视为通用数组方法和对象之间的契约。对象必须满足某些要求否则这些方法将无法在它们上面工作。这些要求是 类数组对象的元素必须可以通过方括号和从 0 开始的整数索引访问。所有方法都需要读取访问权限一些方法还需要写入访问权限。请注意所有对象都支持这种索引方括号中的索引被转换为字符串并用作查找属性值的键js var obj { 0: abc }; obj[0]abc 类数组对象必须有一个length属性其值是其元素的数量。一些方法要求length是可变的例如reverse()。长度不可变的值例如字符串不能与这些方法一起使用。#### 处理类数组对象的模式以下模式对处理类数组对象很有用 将类数组对象转换为数组jsvar arr Array.prototype.slice.call(arguments);方法slice()参见[Concatenating, Slicing, Joining (Nondestructive)](ch18.html#Array.prototype.slice Concatenating, Slicing, Joining (Nondestructive)没有任何参数时会创建一个数组接收者的副本jsvar copy [ a, b ].slice(); 要遍历类数组对象的所有元素可以使用简单的for循环jsfunction logArgs() {for (var i0; iarguments.length; i) {console.log(i. arguments[i]);}}但你也可以借用Array.prototype.forEach()jsfunction logArgs() {Array.prototype.forEach.call(arguments, function (elem, i) {console.log(i. elem);});}在这两种情况下交互如下js logArgs(hello, world);0\. hello1\. world### 通用方法列表以下列表包括所有在 ECMAScript 语言规范中提到的通用方法 Array.prototype参见[Array Prototype Methods](ch18.html#array_prototype_methods Array Prototype Methods) concat every filter forEach indexOf join lastIndexOf map pop push reduce reduceRight reverse shift slice some sort splice toLocaleString toString unshift Date.prototype参见[Date Prototype Methods](ch20.html#date_prototype_methods Date Prototype Methods) toJSON Object.prototype参见[Methods of All Objects](ch17_split_001.html#methods_of_all_objects Methods of All Objects) 所有Object方法都自动是通用的——它们必须适用于所有对象。 String.prototype参见[String Prototype Methods](ch12.html#string_prototype_methods String Prototype Methods) charAt charCodeAt concat indexOf lastIndexOf localeCompare match replace search slice split substring toLocaleLowerCase toLocaleUpperCase toLowerCase toUpperCase trim## 陷阱使用对象作为映射由于 JavaScript 没有内置的映射数据结构对象经常被用作从字符串到值的映射。然而这比看起来更容易出错。本节解释了在这个任务中涉及的三个陷阱。### 陷阱 1继承影响属性读取读取属性的操作可以分为两种 一些操作会考虑整个原型链并查看继承的属性。 其他操作只访问对象的*自有*非继承的属性。当你读取对象作为映射的条目时你需要仔细选择这些操作。为了理解原因考虑以下示例js var proto { protoProp: a }; var obj Object.create(proto); obj.ownProp b;obj是一个具有一个自有属性的对象其原型是protoproto也有一个自有属性。proto的原型是Object.prototype就像所有通过对象文字创建的对象一样。因此obj从proto和Object.继承属性。 我们希望obj被解释为具有单个条目的映射 ownProp: b也就是说我们希望忽略继承的属性只考虑自有属性。让我们看看哪些读取操作以这种方式解释obj哪些不是。请注意对于对象作为映射我们通常希望使用存储在变量中的任意属性键。这排除了点表示法。 检查属性是否存在 in运算符检查对象是否具有给定键的属性但它会考虑继承的属性 ownProp in obj // ok trueunknown in obj // ok falsetoString in obj // wrong, inherited from Object.prototype trueprotoProp in obj // wrong, inherited from proto true我们需要检查以忽略继承的属性。hasOwnProperty()正是我们想要的 obj.hasOwnProperty(ownProp) // ok trueobj.hasOwnProperty(unknown) // ok falseobj.hasOwnProperty(toString) // ok falseobj.hasOwnProperty(protoProp) // ok false收集属性键 我们可以使用什么操作来找到obj的所有键同时又尊重我们对其作为映射的解释for-in看起来可能有效。但是不幸的是它不行 for (propKey in obj) console.log(propKey) ownProp protoProp它会考虑继承的可枚举属性。Object.prototype的属性没有显示在这里的原因是它们都是不可枚举的。 相比之下Object.keys()只列出自有属性 Object.keys(obj) [ ownProp ]这个方法只返回可枚举的自有属性ownProp是通过赋值添加的因此默认情况下是可枚举的。如果你想列出所有自有属性你需要使用Object.getOwnPropertyNames()。 获取属性值 对于读取属性值我们只能在点运算符和括号运算符之间进行选择。我们不能使用前者因为我们有存储在变量中的任意键。这就只剩下了括号运算符它会考虑继承的属性 obj[toString] [Function: toString]这不是我们想要的。没有内置操作可以只读取自有属性但你可以很容易地自己实现一个 function getOwnProperty(obj, propKey) {// Using hasOwnProperty() in this manner is problematic// (explained and fixed later)return (obj.hasOwnProperty(propKey)? obj[propKey] : undefined); }有了这个函数继承的属性toString被忽略了 getOwnProperty(obj, toString) undefined陷阱 2覆盖影响调用方法 函数getOwnProperty()在obj上调用了方法hasOwnProperty()。通常情况下这是可以的 getOwnProperty({ foo: 123 }, foo) 123然而如果你向obj添加一个键为hasOwnProperty的属性那么该属性将覆盖方法Object.prototype.hasOwnProperty()getOwnProperty()将不起作用 getOwnProperty({ hasOwnProperty: 123 }, foo) TypeError: Property hasOwnProperty is not a function你可以通过直接引用hasOwnProperty()来解决这个问题。这避免了通过obj来查找它 function getOwnProperty(obj, propKey) {return (Object.prototype.hasOwnProperty.call(obj, propKey)? obj[propKey] : undefined); }我们已经通用地调用了hasOwnProperty()参见通用方法从原型中借用方法。 陷阱 3特殊属性 proto 在许多 JavaScript 引擎中属性__proto__参见特殊属性 proto是特殊的获取它会检索对象的原型设置它会改变对象的原型。这就是为什么对象不能在键为__proto__的属性中存储映射数据。如果你想允许映射键__proto__你必须在使用它作为属性键之前对其进行转义 function get(obj, key) {return obj[escapeKey(key)]; } function set(obj, key, value) {obj[escapeKey(key)] value; } // Similar: checking if key exists, deleting an entryfunction escapeKey(key) {if (key.indexOf(__proto__) 0) { // (1)return key%;} else {return key;} }我们还需要转义__proto__等等的转义版本以避免冲突也就是说如果我们将键__proto__转义为__proto__%那么我们还需要转义键__proto__%以免它替换__proto__条目。这就是第1行发生的情况。 Mark S. Miller 在一封电子邮件中提到了这个陷阱的现实影响 认为这个练习是学术性的不会在实际系统中出现吗正如在一个支持主题中观察到的直到最近在所有非 IE 浏览器上如果你在新的 Google Doc 开头输入“proto”你的 Google Doc 会卡住。这是因为将对象作为字符串映射的错误使用。 dict 模式没有原型的对象更适合作为映射 你可以这样创建一个没有原型的对象 var dict Object.create(null);这样的对象比普通对象更好的映射字典这就是为什么有时这种模式被称为dict 模式dict代表dictionary。让我们首先检查普通对象然后找出为什么无原型对象是更好的映射。 普通对象 通常您在 JavaScript 中创建的每个对象至少都有Object.prototype在其原型链中。Object.prototype的原型是null因此大多数原型链都在这里结束 Object.getPrototypeOf({}) Object.prototype trueObject.getPrototypeOf(Object.prototype) null无原型对象 无原型对象作为映射有两个优点 继承的属性陷阱1不再是问题因为根本没有。因此您现在可以自由使用in运算符来检测属性是否存在并使用括号来读取属性。 很快__proto__将被禁用。在 ECMAScript 6 中如果Object.prototype不在对象的原型链中特殊属性__proto__将被禁用。您可以期望 JavaScript 引擎慢慢迁移到这种行为但目前还不太常见。 唯一的缺点是您将失去Object.prototype提供的服务。例如dict 对象不再可以自动转换为字符串 console.log(Result: obj) TypeError: Cannot convert object to primitive value但这并不是真正的缺点因为直接在 dict 对象上调用方法是不安全的。 推荐 在快速的 hack 和库的基础上使用 dict 模式。在非库生产代码中库更可取因为您可以确保避免所有陷阱。下一节列出了一些这样的库。 最佳实践 使用对象作为映射有许多应用。如果所有属性键在开发时已经静态知道那么你只需要确保忽略继承只查看自有属性。如果可以使用任意键你应该转向库以避免本节中提到的陷阱。以下是两个例子 Google 的 es-lab的StringMap.js Olov Lassus的stringmap.js 速查表使用对象 本节是一个快速参考指向更详细的解释。 对象字面量参见对象字面量 var jane {name: Jane,not an identifier: 123,describe: function () { // methodreturn Person named this.name;}, }; // Call a method: console.log(jane.describe()); // Person named Jane点运算符.参见点运算符.通过固定键访问属性 obj.propKey obj.propKey value delete obj.propKey括号运算符[]参见括号运算符[]通过计算键访问属性 obj[propKey] obj[propKey] value delete obj[propKey]获取和设置原型参见获取和设置原型 Object.create(proto, propDescObj?) Object.getPrototypeOf(obj)属性的迭代和检测参见属性的迭代和检测 Object.keys(obj) Object.getOwnPropertyNames(obj)Object.prototype.hasOwnProperty.call(obj, propKey) propKey in obj通过描述符获取和定义属性参见通过描述符获取和定义属性 Object.defineProperty(obj, propKey, propDesc) Object.defineProperties(obj, propDescObj) Object.getOwnPropertyDescriptor(obj, propKey) Object.create(proto, propDescObj?)保护对象参见保护对象 Object.preventExtensions(obj) Object.isExtensible(obj)Object.seal(obj) Object.isSealed(obj)Object.freeze(obj) Object.isFrozen(obj)所有对象的方法参见所有对象的方法 Object.prototype.toString() Object.prototype.valueOf()Object.prototype.toLocaleString()Object.prototype.isPrototypeOf(obj) Object.prototype.hasOwnProperty(key) Object.prototype.propertyIsEnumerable(propKey)¹⁵通过这种方式使用map()是 Brandon Benviebenvie的一个提示。 第十八章数组 原文18. Arrays 译者飞龙 协议CC BY-NC-SA 4.0 数组是从索引从零开始的自然数到任意值的映射。值映射的范围称为数组的元素。创建数组的最方便的方法是通过数组字面量。这样的字面量列举了数组元素元素的位置隐含地指定了它的索引。 在本章中我将首先介绍基本的数组机制如索引访问和length属性然后再介绍数组方法。 概述 本节提供了数组的快速概述。详细内容将在后面解释。 作为第一个例子我们通过数组字面量创建一个数组 arr参见[创建数组](ch18.html#creating_arrays “Creating Arrays”并访问元素参见[数组索引](ch18.html#array_indices “Array Indices” var arr [ a, b, c ]; // array literalarr[0] // get element 0 aarr[0] x; // set element 0arr [ x, b, c ]我们可以使用数组属性 length参见length来删除和追加元素 var arr [ a, b, c ];arr.length 3arr.length 2; // remove an elementarr [ a, b ]arr[arr.length] d; // append an elementarr [ a, b, d ]数组方法 push() 提供了另一种追加元素的方式 var arr [ a, b ];arr.push(d) 3arr [ a, b, d ]数组是映射不是元组 ECMAScript 标准将数组规定为从索引到值的映射字典。换句话说数组可能不是连续的并且可能有空洞。例如 var arr [];arr[0] a; aarr[2] b; barr [ a, , b ]前面的数组有一个空洞索引 1 处没有元素。数组中的空洞 更详细地解释了空洞。 请注意大多数 JavaScript 引擎会在内部优化没有空洞的数组并将它们连续存储。 数组也可以有属性 数组仍然是对象可以有对象属性。这些属性不被视为实际数组的一部分也就是说它们不被视为数组元素 var arr [ a, b ];arr.foo 123;arr [ a, b ]arr.foo 123创建数组 你可以通过数组字面量创建一个数组 var myArray [ a, b, c ];数组中的尾随逗号会被忽略 [ a, b ].length 2[ a, b, ].length 2[ a, b, ,].length // hole trailing comma 3数组构造函数 有两种使用构造函数 Array 的方式可以创建一个给定长度的空数组或者数组的元素是给定的值。对于这个构造函数new 是可选的以普通函数的方式调用它不带 new与以构造函数的方式调用它是一样的。 创建一个给定长度的空数组 给定长度的空数组中只有空洞因此很少有意义使用这个版本的构造函数 var arr new Array(2);arr.length 2arr // two holes plus trailing comma (ignored!) [ , ,]一些引擎在以这种方式调用 Array() 时可能会预先分配连续的内存这可能会稍微提高性能。但是请确保增加的冗余性值得 初始化带有元素的数组避免 这种调用 Array 的方式类似于数组字面量 // The same as [a, b, c]: var arr1 new Array(a, b, c);问题在于你不能创建只有一个数字的数组因为那会被解释为创建一个 length 为该数字的数组 new Array(2) // alas, not [ 2 ] [ , ,] new Array(5.7) // alas, not [ 5.7 ] RangeError: Invalid array length new Array(abc) // ok [ abc ]多维数组 如果你需要为元素创建多个维度你必须嵌套数组。当你创建这样的嵌套数组时最内层的数组可以根据需要增长。但是如果你想直接访问元素你至少需要创建外部数组。在下面的例子中我为井字游戏创建了一个三乘三的矩阵。该矩阵完全填满了数据而不是让行根据需要增长 // Create the Tic-tac-toe board var rows []; for (var rowCount0; rowCount 3; rowCount) {rows[rowCount] [];for (var colCount0; colCount 3; colCount) {rows[rowCount][colCount] .;} }// Set an X in the upper right corner rows[0][2] X; // [row][column]// Print the board rows.forEach(function (row) {console.log(row.join( )); });以下是输出 . . X . . . . . .我希望这个例子能够演示一般情况。显然如果矩阵很小并且具有固定的维度你可以通过数组字面量来设置它 var rows [ [.,.,.], [.,.,.], [.,.,.] ];数组索引 当你使用数组索引时你必须牢记以下限制 索引是范围在 0 ≤ i 2³²−1 的数字 i。 最大长度为 2³²−1。 超出范围的索引被视为普通的属性键字符串。它们不会显示为数组元素也不会影响属性 length。例如 var arr []; arr[-1] a;arr []arr[-1] a arr[4294967296] b;arr []arr[4294967296] bin 操作符和索引 in 操作符用于检测对象是否具有给定键的属性。但它也可以用于确定数组中是否存在给定的元素索引。例如 var arr [ a, , b ];0 in arr true1 in arr false10 in arr false删除数组元素 除了删除属性之外delete 操作符还可以删除数组元素。删除元素会创建空洞length 属性不会更新 var arr [ a, b ];arr.length 2delete arr[1] // does not update length truearr [ a, ]arr.length 2你也可以通过减少数组的长度来删除尾随的数组元素参见length了解详情。要删除元素而不创建空洞即后续元素的索引被减少你可以使用 Array.prototype.splice()参见添加和删除元素破坏性。在这个例子中我们删除索引为 1 的两个元素 var arr [a, b, c, d];arr.splice(1, 2) // returns what has been removed [ b, c ]arr [ a, d ]数组索引详解 提示 这是一个高级部分。通常情况下您不需要知道这里解释的细节。 数组索引并非看起来那样。 到目前为止我一直假装数组索引是数字。这也是 JavaScript 引擎在内部实现数组的方式。然而ECMAScript 规范对索引的看法不同。引用第 15.4 节的话来说 如果且仅当ToString(ToUint32(P))等于P且ToUint32(P)不等于 2³²−1 时属性键P一个字符串才是数组索引。这意味着什么将在下面解释。 属性键为数组索引的数组属性称为元素。 换句话说在规范中括号中的所有值都被转换为字符串并解释为属性键甚至是数字。以下互动演示了这一点 var arr [a, b];arr[0] aarr[0] a要成为数组索引属性键P一个字符串必须等于以下计算结果 将P转换为数字。 将数字转换为 32 位无符号整数。 将整数转换为字符串。 这意味着数组索引必须是 32 位范围内的字符串化整数i其中 0 ≤ i 2³²−1。规范明确排除了上限如前面引用的。它保留给了最大长度。要了解这个定义是如何工作的让我们使用通过位运算符实现 32 位整数中的ToUint32()函数。 首先不包含数字的字符串总是转换为 0这在字符串化后不等于字符串 ToUint32(xyz) 0ToUint32(?#!) 0其次超出范围的字符串化整数也会转换为完全不同的整数与字符串化后不相等 ToUint32(-1) 4294967295Math.pow(2, 32) 4294967296ToUint32(4294967296) 0第三字符串化的非整数数字会转换为整数这些整数又是不同的 ToUint32(1.371) 1请注意规范还强制规定数组索引不得具有指数 ToUint32(1e3) 1000它们不包含前导零 var arr [a, b];arr[0] // array index aarr[00] // normal property undefined长度 length属性的基本功能是跟踪数组中的最高索引 [ a, b ].length 2[ a, , b ].length 3因此length不计算元素的数量因此您必须编写自己的函数来执行此操作。例如 function countElements(arr) {var elemCount 0;arr.forEach(function () {elemCount;});return elemCount; }为了计算元素非空洞我们已经利用了forEach跳过空洞的事实。以下是互动 countElements([ a, b ]) 2countElements([ a, , b ]) 2手动增加数组的长度 手动增加数组的长度对数组几乎没有影响它只会创建空洞 var arr [ a, b ];arr.length 3;arr // one hole at the end [ a, b, ,]最后的结果末尾有两个逗号因为尾随逗号是可选的因此总是被忽略。 我们刚刚做的并没有添加任何元素 countElements(arr) 2然而length属性确实作为指针指示在哪里插入新元素。例如 arr.push(c) 4arr [ a, b, , c ]因此通过Array构造函数设置数组的初始长度会创建一个完全空的数组 var arr new Array(2);arr.length 2countElements(arr) 0减少数组的长度 如果您减少数组的长度则新长度及以上的所有元素都将被删除 var arr [ a, b, c ];1 in arr truearr[1] b arr.length 1;arr [ a ]1 in arr falsearr[1] undefined清除数组 如果将数组的长度设置为 0则它将变为空。这样可以清除数组。例如 function clearArray(arr) {arr.length 0; }以下是互动 var arr [ a, b, c ];clearArray(arr)arr []但是请注意这种方法可能会很慢因为每个数组元素都会被显式删除。具有讽刺意味的是创建一个新的空数组通常更快 arr [];清除共享数组 您需要知道的是将数组的长度设置为零会影响共享数组的所有人 var a1 [1, 2, 3];var a2 a1;a1.length 0; a1 []a2 []相比之下分配一个空数组不会 var a1 [1, 2, 3];var a2 a1;a1 []; a1 []a2 [ 1, 2, 3 ]最大长度 最大数组长度为 2³²−1 var arr1 new Array(Math.pow(2, 32)); // not ok RangeError: Invalid array length var arr2 new Array(Math.pow(2, 32)-1); // okarr2.push(x); RangeError: Invalid array length数组中的空洞 数组是从索引到值的映射。这意味着数组可以有空洞即长度小于数组中缺失的索引。在这些索引中读取元素会返回undefined。 提示 建议避免数组中的空洞。JavaScript 对它们的处理不一致即一些方法忽略它们其他方法不会。幸运的是通常你不需要知道如何处理空洞它们很少有用并且会对性能产生负面影响。 创建空洞 通过给数组索引赋值可以创建空洞 var arr [];arr[0] a;arr[2] c;1 in arr // hole at index 1 false也可以通过在数组字面量中省略值来创建空洞 var arr [a,,c];1 in arr // hole at index 1 false警告 需要两个尾随逗号来创建尾随的空洞因为最后一个逗号总是被忽略 [ a, ].length 1[ a, ,].length 2稀疏数组与密集数组 本节将检查空洞和undefined作为元素之间的区别。鉴于读取空洞会返回undefined两者非常相似。 带有空洞的数组称为稀疏数组。没有空洞的数组称为密集数组。密集数组是连续的并且在每个索引处都有一个元素——从零开始到length-1 结束。让我们比较以下两个数组一个是稀疏数组一个是密集数组。这两者非常相似 var sparse [ , , c ]; var dense [ undefined, undefined, c ];空洞几乎就像在相同索引处有一个undefined元素。两个数组的长度都是一样的 sparse.length 3dense.length 3但是稀疏数组没有索引为 0 的元素 0 in sparse false0 in dense true通过for进行迭代对两个数组来说是一样的 for (var i0; isparse.length; i) console.log(sparse[i]); undefined undefined cfor (var i0; idense.length; i) console.log(dense[i]); undefined undefined c通过forEach进行迭代会跳过空洞但不会跳过未定义的元素 sparse.forEach(function (x) { console.log(x) }); cdense.forEach(function (x) { console.log(x) }); undefined undefined c哪些操作会忽略空洞哪些会考虑它们 涉及数组的一些操作会忽略空洞而另一些会考虑它们。本节解释了细节。 数组迭代方法 forEach()会跳过空洞 [a,, b].forEach(function (x,i) { console.log(i.x) }) 0.a 2.bevery()也会跳过空洞类似的some() [a,, b].every(function (x) { return typeof x string }) truemap()会跳过但保留空洞 [a,, b].map(function (x,i) { return i.x }) [ 0.a, , 2.b ]filter()消除空洞 [a,, b].filter(function (x) { return true }) [ a, b ]其他数组方法 join()将空洞、undefined和null转换为空字符串 [a,, b].join(-) a--b[ a, undefined, b ].join(-) a--bsort()在排序时保留空洞 [a,, b].sort() // length of result is 3 [ a, b, , ]for-in 循环 for-in循环正确列出属性键它们是数组索引的超集 for (var key in [a,, b]) { console.log(key) } 0 2Function.prototype.apply() apply()将每个空洞转换为一个值为undefined的参数。以下交互演示了这一点函数f()将其参数作为数组返回。当我们传递一个带有三个空洞的数组给apply()以调用f()时后者接收到三个undefined参数 function f() { return [].slice.call(arguments) }f.apply(null, [ , , ,]) [ undefined, undefined, undefined ]这意味着我们可以使用apply()来创建一个带有undefined的数组 Array.apply(null, Array(3)) [ undefined, undefined, undefined ]警告 apply()将空洞转换为undefined在空数组中但不能用于在任意数组中填补空洞可能包含或不包含空洞。例如任意数组[2] Array.apply(null, [2]) [ , ,]数组不包含任何空洞所以apply()应该返回相同的数组。但实际上它返回一个长度为 2 的空数组它只包含两个空洞。这是因为Array()将单个数字解释为数组长度而不是数组元素。 从数组中移除空洞 正如我们所见filter()会移除空洞 [a,, b].filter(function (x) { return true }) [ a, b ]使用自定义函数将任意数组中的空洞转换为undefined function convertHolesToUndefineds(arr) {var result [];for (var i0; i arr.length; i) {result[i] arr[i];}return result; }使用该函数 convertHolesToUndefineds([a,, b]) [ a, undefined, b ]数组构造方法 Array.isArray(obj) 如果obj是数组则返回true。它正确处理跨realms窗口或框架的对象——与instanceof相反参见Pitfall: crossing realms (frames or windows)。 数组原型方法 在接下来的章节中数组原型方法按功能分组。对于每个子章节我会提到这些方法是破坏性的它们会改变被调用的数组还是非破坏性的它们不会修改它们的接收者这样的方法通常会返回新的数组。 添加和删除元素破坏性 本节中的所有方法都是破坏性的 Array.prototype.shift() 删除索引为 0 的元素并返回它。随后元素的索引减 1 var arr [ a, b ];arr.shift() aarr [ b ]Array.prototype.unshift(elem1?, elem2?, ...) 将给定的元素添加到数组的开头。它返回新的长度 var arr [ c, d ];arr.unshift(a, b) 4arr [ a, b, c, d ]Array.prototype.pop() 移除数组的最后一个元素并返回它 var arr [ a, b ];arr.pop() barr [ a ]Array.prototype.push(elem1?, elem2?, ...) 将给定的元素添加到数组的末尾。它返回新的长度 var arr [ a, b ];arr.push(c, d) 4arr [ a, b, c, d ]apply()参见Function.prototype.apply(thisValue, argArray)使您能够破坏性地将数组arr2附加到另一个数组arr1 var arr1 [ a, b ];var arr2 [ c, d ]; Array.prototype.push.apply(arr1, arr2) 4arr1 [ a, b, c, d ]Array.prototype.splice(start, deleteCount?, elem1?, elem2?, ...) 从start开始删除deleteCount个元素并插入给定的元素。换句话说您正在用elem1、elem2等替换位置start处的deleteCount个元素。该方法返回已被移除的元素 var arr [ a, b, c, d ];arr.splice(1, 2, X); [ b, c ]arr [ a, X, d ]特殊的参数值 start可以为负数这种情况下它将被加到长度以确定起始索引。因此-1指的是最后一个元素依此类推。 deleteCount是可选的。如果省略以及所有后续参数则删除从索引start开始的所有元素及之后的所有元素。 在此示例中我们删除最后两个索引后的所有元素 var arr [ a, b, c, d ];arr.splice(-2) [ c, d ]arr [ a, b ]排序和颠倒元素破坏性 这些方法也是破坏性的 Array.prototype.reverse() 颠倒数组中元素的顺序并返回对原始修改后的数组的引用 var arr [ a, b, c ];arr.reverse() [ c, b, a ]arr // reversing happened in place [ c, b, a ]Array.prototype.sort(compareFunction?) 对数组进行排序并返回它 var arr [banana, apple, pear, orange];arr.sort() [ apple, banana, orange, pear ]arr // sorting happened in place [ apple, banana, orange, pear ]请记住排序通过将值转换为字符串进行比较这意味着数字不会按数字顺序排序 [-1, -20, 7, 50].sort() [ -1, -20, 50, 7 ]您可以通过提供可选参数compareFunction来解决这个问题它控制排序的方式。它具有以下签名 function compareFunction(a, b)此函数比较a和b并返回 如果a小于b则返回小于零的整数例如-1 如果a等于b则返回零 如果a大于b则返回大于零的整数例如1 比较数字 对于数字您可以简单地返回a-b但这可能会导致数值溢出。为了防止这种情况发生您需要更冗长的代码 function compareCanonically(a, b) {if (a b) {return -1;} else if (a b) {return 1;} else {return 0;} }我不喜欢嵌套的条件运算符。但在这种情况下代码要简洁得多我很想推荐它 function compareCanonically(a, b) {return return a b ? -1 (a b ? 1 : 0); }使用该函数 [-1, -20, 7, 50].sort(compareCanonically) [ -20, -1, 7, 50 ]比较字符串 对于字符串您可以使用String.prototype.localeCompare参见比较字符串 [c, a, b].sort(function (a,b) { return a.localeCompare(b) }) [ a, b, c ]比较对象 参数compareFunction对于排序对象也很有用 var arr [{ name: Tarzan },{ name: Cheeta },{ name: Jane } ];function compareNames(a,b) {return a.name.localeCompare(b.name); }使用compareNames作为比较函数arr按name排序 arr.sort(compareNames) [ { name: Cheeta },{ name: Jane },{ name: Tarzan } ]连接、切片、连接非破坏性 以下方法对数组执行各种非破坏性操作 Array.prototype.concat(arr1?, arr2?, ...) 创建一个新数组其中包含接收器的所有元素后跟数组arr1的所有元素依此类推。如果其中一个参数不是数组则将其作为元素添加到结果中例如这里的第一个参数c var arr [ a, b ];arr.concat(c, [d, e]) [ a, b, c, d, e ]调用concat()的数组不会改变 arr [ a, b ]Array.prototype.slice(begin?, end?) 将数组元素复制到一个新数组中从begin开始直到end之前的元素 [ a, b, c, d ].slice(1, 3) [ b, c ]如果缺少end则使用数组长度 [ a, b, c, d ].slice(1) [ b, c, d ]如果两个索引都缺失则复制数组 [ a, b, c, d ].slice() [ a, b, c, d ]如果任一索引为负数则将数组长度加上它。因此-1指的是最后一个元素依此类推 [ a, b, c, d ].slice(1, -1) [ b, c ][ a, b, c, d ].slice(-2) [ c, d ]Array.prototype.join(separator?) 通过对所有数组元素应用toString()并在结果之间放置separator字符串来创建一个字符串。如果省略separator则使用, [3, 4, 5].join(-) 3-4-5[3, 4, 5].join() 3,4,5[3, 4, 5].join() 345join()将undefined和null转换为空字符串 [undefined, null].join(#) #数组中的空位也会转换为空字符串 [a,, b].join(-) a--b搜索值非破坏性 以下方法在数组中搜索值 Array.prototype.indexOf(searchValue, startIndex?) 从startIndex开始搜索数组中的searchValue。它返回第一次出现的索引如果找不到则返回-1。如果startIndex为负数则将数组长度加上它如果缺少startIndex则搜索整个数组 [ 3, 1, 17, 1, 4 ].indexOf(1) 1[ 3, 1, 17, 1, 4 ].indexOf(1, 2) 3搜索时使用严格相等参见相等运算符与这意味着indexOf()无法找到NaN [NaN].indexOf(NaN) -1Array.prototype.lastIndexOf(searchElement, startIndex?) 在startIndex开始向后搜索searchElement返回第一次出现的索引或-1如果找不到。如果startIndex为负数则将数组长度加上它如果缺失则搜索整个数组。搜索时使用严格相等参见相等运算符与 [ 3, 1, 17, 1, 4 ].lastIndexOf(1) 3[ 3, 1, 17, 1, 4 ].lastIndexOf(1, -3) 1迭代非破坏性 迭代方法使用一个函数来迭代数组。我区分三种迭代方法它们都是非破坏性的检查方法主要观察数组的内容转换方法从接收器派生一个新数组减少方法基于接收器的元素计算结果。 检查方法 本节中描述的每个方法都是这样的 arr.examinationMethod(callback, thisValue?)这样的方法需要以下参数 callback是它的第一个参数一个它调用的函数。根据检查方法的不同回调返回布尔值或无返回值。它具有以下签名 function callback(element, index, array)element是callback要处理的数组元素index是元素的索引array是调用examinationMethod的数组。 thisValue允许您配置callback内部的this的值。 现在是我刚刚描述的检查方法的签名 Array.prototype.forEach(callback, thisValue?) 迭代数组的元素 var arr [ apple, pear, orange ]; arr.forEach(function (elem) {console.log(elem); });Array.prototype.every(callback, thisValue?) 如果回调对每个元素返回true则返回true。一旦回调返回false迭代就会停止。请注意不返回值会导致隐式返回undefinedevery()将其解释为false。every()的工作方式类似于全称量词“对于所有”。 这个例子检查数组中的每个数字是否都是偶数 function isEven(x) { return x % 2 0 }[ 2, 4, 6 ].every(isEven) true[ 2, 3, 4 ].every(isEven) false如果数组为空则结果为true并且不调用callback [].every(function () { throw new Error() }) trueArray.prototype.some(callback, thisValue?) 如果回调对至少一个元素返回true则返回true。一旦回调返回true迭代就会停止。请注意不返回值会导致隐式返回undefinedsome()将其解释为false。some()的工作方式类似于存在量词“存在”。 这个例子检查数组中是否有偶数 function isEven(x) { return x % 2 0 }[ 1, 3, 5 ].some(isEven) false[ 1, 2, 3 ].some(isEven) true如果数组为空则结果为false并且不调用callback [].some(function () { throw new Error() }) falseforEach()的一个潜在陷阱是它不支持break或类似的东西来提前中止循环。如果你需要这样做可以使用some() function breakAtEmptyString(strArr) {strArr.some(function (elem) {if (elem.length 0) {return true; // break}console.log(elem);// implicit: return undefined (interpreted as false)}); }some()如果发生了中断则返回true否则返回false。这使您可以根据迭代是否成功完成这在for循环中有点棘手做出不同的反应。 转换方法 转换方法接受一个输入数组并产生一个输出数组而回调控制输出的产生方式。回调的签名与检查方法相同 function callback(element, index, array)有两种转换方法 Array.prototype.map(callback, thisValue?) 每个输出数组元素是将callback应用于输入元素的结果。例如 [ 1, 2, 3 ].map(function (x) { return 2 * x }) [ 2, 4, 6 ]Array.prototype.filter(callback, thisValue?) 输出数组仅包含那些callback返回true的输入元素。例如 [ 1, 0, 3, 0 ].filter(function (x) { return x ! 0 }) [ 1, 3 ]减少方法 对于减少回调具有不同的签名 function callback(previousValue, currentElement, currentIndex, array)参数previousValue是回调先前返回的值。当首次调用回调时有两种可能性描述适用于Array.prototype.reduce()与reduceRight()的差异在括号中提到 提供了明确的initialValue。然后previousValue是initialValuecurrentElement是第一个数组元素reduceRight最后一个数组元素。 没有提供明确的initialValue。然后previousValue是第一个数组元素currentElement是第二个数组元素reduceRight最后一个数组元素和倒数第二个数组元素。 有两种减少的方法 Array.prototype.reduce(callback, initialValue?) 从左到右迭代并像之前描述的那样调用回调。该方法的结果是回调返回的最后一个值。此示例计算所有数组元素的总和 function add(prev, cur) {return prev cur; } console.log([10, 3, -1].reduce(add)); // 12如果你在一个只有一个元素的数组上调用reduce那么该元素会被返回 [7].reduce(add) 7如果你在一个空数组上调用reduce你必须指定initialValue否则你会得到一个异常 [].reduce(add) TypeError: Reduce of empty array with no initial value[].reduce(add, 123) 123Array.prototype.reduceRight(callback, initialValue?) 与reduce()相同但从右到左迭代。 注意 在许多函数式编程语言中reduce被称为fold或foldl左折叠reduceRight被称为foldr右折叠。 看reduce方法的另一种方式是它实现了一个 n 元运算符OP OP[1≤i≤n] x[i] 通过一系列二元运算符op2的应用 (…(x[1] op2 x[2]) op2 …) op2 x[n] 这就是前面代码示例中发生的事情我们通过 JavaScript 的二进制加法运算符实现了一个数组的 n 元求和运算符。 例如让我们通过以下函数来检查两个迭代方向 function printArgs(prev, cur, i) {console.log(prev:prev, cur:cur, i:i);return prev cur; }如预期的那样reduce()从左到右迭代 [a, b, c].reduce(printArgs) prev:a, cur:b, i:1 prev:ab, cur:c, i:2 abc[a, b, c].reduce(printArgs, x) prev:x, cur:a, i:0 prev:xa, cur:b, i:1 prev:xab, cur:c, i:2 xabcreduceRight()从右到左迭代 [a, b, c].reduceRight(printArgs) prev:c, cur:b, i:1 prev:cb, cur:a, i:0 cba[a, b, c].reduceRight(printArgs, x) prev:x, cur:c, i:2 prev:xc, cur:b, i:1 prev:xcb, cur:a, i:0 xcba陷阱类数组对象 JavaScript 中的一些对象看起来像数组但它们并不是数组。这通常意味着它们具有索引访问和length属性但没有数组方法。例子包括特殊变量argumentsDOM 节点列表和字符串。类数组对象和通用方法提供了处理类数组对象的提示。 最佳实践迭代数组 要迭代一个数组arr你有两个选择 一个简单的for循环参见for for (var i0; iarr.length; i) {console.log(arr[i]); }数组迭代方法之一参见迭代非破坏性。例如forEach() arr.forEach(function (elem) {console.log(elem); });不要使用for-in循环参见for-in来迭代数组。它遍历索引而不是值。在这样做的同时它包括正常属性的键包括继承的属性。 第十九章正则表达式 原文19. Regular Expressions 译者飞龙 协议CC BY-NC-SA 4.0 本章概述了正则表达式的 JavaScript API。它假定你对它们的工作原理有一定了解。如果你不了解网上有很多好的教程。其中两个例子是 Regular-Expressions.info by Jan Goyvaerts Cody Lindley 的 JavaScript 正则表达式启示 正则表达式语法 这里使用的术语与 ECMAScript 规范中的语法非常接近。我有时会偏离以使事情更容易理解。 原子一般 一般原子的语法如下 特殊字符 以下所有字符都具有特殊含义 \ ^ $ . * ? ( ) [ ] { } |你可以通过在前面加上反斜杠来转义它们。例如 /^(ab)$/.test((ab)) false/^\(ab\)$/.test((ab)) true其他特殊字符包括 在字符类[...]中 -在以问号(?...)开头的组内 : ! 尖括号仅由 XRegExp 库参见第三十章使用用于命名组。 模式字符 除了前面提到的特殊字符之外所有字符都与它们自己匹配。 .点 匹配任何 JavaScript 字符UTF-16 代码单元除了行终止符换行符、回车符等。要真正匹配任何字符请使用[\s\S]。例如 /./.test(\n) false/[\s\S]/.test(\n) true字符转义匹配单个字符 特定的控制字符包括\f换页符、\n换行符、新行、\r回车符、\t水平制表符和\v垂直制表符。 \0匹配 NUL 字符\u0000。 任何控制字符\cA – \cZ。 Unicode 字符转义\u0000 – \xFFFFUnicode 代码单元参见第二十四章。 十六进制字符转义\x00 – \xFF。 字符类转义匹配一组字符中的一个 数字\d匹配任何数字与[0-9]相同\D匹配任何非数字与[^0-9]相同。 字母数字字符\w匹配任何拉丁字母数字字符加下划线与[A-Za-z0-9_]相同\W匹配所有未被\w匹配的字符。 空白\s匹配空白字符空格、制表符、换行符、回车符、换页符、所有 Unicode 空格等\S匹配所有非空白字符。 原子字符类 字符类的语法如下 [«charSpecs»]匹配至少一个charSpecs中的任何一个的单个字符。 [^«charSpecs»]匹配任何不匹配charSpecs中任何一个的单个字符。 以下构造都是字符规范 源字符匹配它们自己。大多数字符都是源字符甚至许多在其他地方是特殊的字符。只有三个字符不是 \ ] -像往常一样您可以通过反斜杠进行转义。如果要匹配破折号而不进行转义它必须是方括号打开后的第一个字符或者是范围的右侧如下所述。 类转义允许使用先前列出的任何字符转义和字符类转义。还有一个额外的转义 退格\b在字符类之外\b匹配单词边界。在字符类内它匹配控制字符退格。 范围包括源字符或类转义后跟破折号-后跟源字符或类转义。 为了演示使用字符类此示例解析了按照 ISO 8601 标准格式化的日期 function parseIsoDate(str) {var match /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(str);// Other ways of writing the regular expression:// /^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$/// /^(\d\d\d\d)-(\d\d)-(\d\d)$/if (!match) {throw new Error(Not an ISO date: str);}console.log(Year: match[1]);console.log(Month: match[2]);console.log(Day: match[3]); }以下是交互 parseIsoDate(2001-12-24) Year: 2001 Month: 12 Day: 24原子组 组的语法如下 («pattern»)是一个捕获组。由pattern匹配的任何内容都可以通过反向引用或作为匹配操作的结果来访问。 (?:«pattern»)是一个非捕获组。pattern仍然与输入匹配但不保存为捕获。因此该组没有您可以引用的编号例如通过反向引用。 \1、\2等被称为反向引用它们指回先前匹配的组。反斜杠后面的数字可以是大于或等于 1 的任何整数但第一个数字不能是 0。 在此示例中反向引用保证了破号之前和之后的 a 的数量相同 /^(a)-\1$/.test(a-a) true/^(a)-\1$/.test(aaa-aaa) true/^(a)-\1$/.test(aa-a) false此示例使用反向引用来匹配 HTML 标签显然通常应使用适当的解析器来处理 HTML var tagName /([^])[^]*\/\1/;tagName.exec(bbold/b)[1] btagName.exec(strongtext/strong)[1] strongtagName.exec(strongtext/stron) null量词 任何原子包括字符类和组都可以后跟一个量词 ?表示匹配零次或一次。 *表示匹配零次或多次。 表示匹配一次或多次。 {n}表示精确匹配n次。 {n,}表示匹配n次或更多次。 {n,m}表示至少匹配n次最多匹配m次。 默认情况下量词是贪婪的也就是说它们尽可能多地匹配。您可以通过在任何前述量词包括大括号中的范围后加上问号?来获得勉强匹配尽可能少。例如 a strong.match(/^(.*)/)[1] // greedy a stronga strong.match(/^(.*?)/)[1] // reluctant a因此.*?是一个用于匹配直到下一个原子出现的有用模式。例如以下是刚刚显示的 HTML 标签的正则表达式的更紧凑版本使用[^]*代替.*? /(.?).*?\/\1/断言 断言如下列表所示是关于输入中当前位置的检查 ^仅在输入的开头匹配。$仅在输入的末尾匹配。\b仅在单词边界处匹配。不要与[\b]混淆它匹配退格。\B仅当不在单词边界时匹配。(?«pattern»)正向预查仅当“模式”匹配接下来的内容时才匹配。“模式”仅用于向前查看否则会被忽略。(?!«pattern»)负向预查仅当“模式”不匹配接下来的内容时才匹配。“模式”仅用于向前查看否则会被忽略。 此示例通过\b匹配单词边界 /\bell\b/.test(hello) false/\bell\b/.test(ello) false/\bell\b/.test(ell) true此示例通过\B匹配单词内部 /\Bell\B/.test(ell) false/\Bell\B/.test(hell) false/\Bell\B/.test(hello) true注意 不支持后行断言。手动实现后行断言解释了如何手动实现它。 分歧 分歧运算符|分隔两个选择任一选择必须匹配分歧才能匹配。这些选择是原子可选包括量词。 该运算符的绑定非常弱因此您必须小心以确保选择不会延伸太远。例如以下正则表达式匹配所有以aa开头或以bb结尾的字符串 /^aa|bb$/.test(aaxx) true/^aa|bb$/.test(xxbb) true换句话说分歧比甚至^和$都要弱两个选择是^aa和bb$。如果要匹配两个字符串aa和bb则需要括号 /^(aa|bb)$/同样如果要匹配字符串aab和abb /^a(a|b)b$/Unicode 和正则表达式 JavaScript 的正则表达式对 Unicode 的支持非常有限。特别是当涉及到星际飞船中的代码点时您必须小心。第二十四章解释了详细信息。 创建正则表达式 您可以通过文字或构造函数创建正则表达式并通过标志配置其工作方式。 文字与构造函数 有两种方法可以创建正则表达式您可以使用文字或构造函数RegExp 文字/xyz/i在加载时编译 构造函数第二个参数是可选的new RegExp(xyzi)在运行时编译 文字和构造函数在编译时不同 文字在加载时编译。在评估时以下代码将引发异常 function foo() {/[/; }构造函数在调用时编译正则表达式。以下代码不会引发异常但调用foo()会 function foo() {new RegExp([); }因此通常应使用文字但如果要动态组装正则表达式则需要构造函数。 标志 标志是正则表达式文字的后缀和正则表达式构造函数的参数它们修改正则表达式的匹配行为。存在以下标志 短名称长名称描述g全局给定的正则表达式多次匹配。影响几种方法特别是replace()。i忽略大小写在尝试匹配给定的正则表达式时忽略大小写。m多行模式在多行模式下开始运算符^和结束运算符$匹配每一行而不是完整的输入字符串。 短名称用于文字前缀和构造函数参数请参见下一节中的示例。长名称用于正则表达式的属性指示在创建期间设置了哪些标志。 正则表达式的实例属性 正则表达式具有以下实例属性 标志表示设置了哪些标志的布尔值 全局标志/g设置了吗 忽略大小写标志/i设置了吗 多行标志/m设置了吗 用于多次匹配的数据设置了/g标志 lastIndex是下次继续搜索的索引。 以下是访问标志的实例属性的示例 var regex /abc/i;regex.ignoreCase trueregex.multiline false创建正则表达式的示例 在这个例子中我们首先使用文字创建相同的正则表达式然后使用构造函数并使用test()方法来确定它是否匹配一个字符串 /abc/.test(ABC) falsenew RegExp(abc).test(ABC) false在这个例子中我们创建一个忽略大小写的正则表达式标志/i /abc/i.test(ABC) truenew RegExp(abc, i).test(ABC) trueRegExp.prototype.test是否有匹配 test()方法检查正则表达式regex是否匹配字符串str regex.test(str)test()的操作方式取决于标志/g是否设置。 如果标志/g未设置则该方法检查str中是否有匹配。例如 var str _x_x; /x/.test(str) true/a/.test(str) false如果设置了标志/g则该方法返回true直到str中regex的匹配次数。属性regex.lastIndex包含最后匹配后的索引 var regex /x/g;regex.lastIndex 0 regex.test(str) trueregex.lastIndex 2 regex.test(str) trueregex.lastIndex 4 regex.test(str) falseString.prototype.search有匹配的索引吗 search()方法在str中查找与regex匹配的内容 str.search(regex)如果有匹配返回找到匹配的索引。否则结果为-1。regex的global和lastIndex属性在执行搜索时被忽略lastIndex不会改变。 例如 abba.search(/b/) 1abba.search(/x/) -1如果search()的参数不是正则表达式则会转换为正则表达式 aaab.search(^ab$) 0RegExp.prototype.exec捕获组 以下方法调用在匹配regex和str时捕获组 var matchData regex.exec(str);如果没有匹配matchData为null。否则matchData是一个匹配结果一个带有两个额外属性的数组 数组元素 元素 0 是完整正则表达式的匹配如果愿意的话是第 0 组。 元素n 1 是第n组的捕获。 属性 input是完整的输入字符串。 index是找到匹配的索引。 第一个匹配标志/g 未设置 如果标志/g未设置则只返回第一个匹配 var regex /a(b)/;regex.exec(_abbb_ab_) [ abbb,bbb,index: 1,input: _abbb_ab_ ]regex.lastIndex 0所有匹配标志/g 设置 如果设置了标志/g则如果反复调用exec()所有匹配都会被返回。返回值null表示没有更多的匹配。属性lastIndex指示下次匹配将继续的位置 var regex /a(b)/g;var str _abbb_ab_; regex.exec(str) [ abbb,bbb,index: 1,input: _abbb_ab_ ]regex.lastIndex 6 regex.exec(str) [ ab,b,index: 7,input: _abbb_ab_ ]regex.lastIndex 10 regex.exec(str) null在这里我们循环匹配 var regex /a(b)/g; var str _abbb_ab_; var match; while (match regex.exec(str)) {console.log(match[1]); }我们得到以下输出 bbb bString.prototype.match捕获组或返回所有匹配的子字符串 以下方法调用匹配regex和str var matchData str.match(regex);如果regex的标志/g未设置此方法的工作方式类似于RegExp.prototype.exec() abba.match(/a/) [ a, index: 0, input: abba ]如果设置了标志则该方法返回一个包含str中所有匹配子字符串的数组即每次匹配的第 0 组如果没有匹配则返回null abba.match(/a/g) [ a, a ]abba.match(/x/g) nullString.prototype.replace搜索和替换 replace()方法搜索字符串str找到与search匹配的内容并用replacement替换它们 str.replace(search, replacement)有几种方式可以指定这两个参数 search 可以是字符串或正则表达式 字符串在输入字符串中直接查找。请注意只有第一次出现的字符串会被替换。如果要替换多个出现必须使用带有/g标志的正则表达式。这是一个意外和一个主要的陷阱。 正则表达式与输入字符串匹配。警告使用global标志否则只会尝试一次匹配正则表达式。 replacement 可以是字符串或函数 字符串描述如何替换已找到的内容。 功能通过参数提供匹配信息来计算替换。 替换是一个字符串 如果replacement是一个字符串它的内容将被直接使用来替换匹配。唯一的例外是特殊字符美元符号$它启动所谓的替换指令 组$n插入匹配中的第 n 组。n必须至少为 1$0没有特殊含义。 匹配的子字符串 $反引号插入匹配前的文本。 $插入完整的匹配。 $撇号插入匹配后的文本。 $$插入一个单独的$。 这个例子涉及匹配的子字符串及其前缀和后缀 axb cxd.replace(/x/g, [$,$,$]) a[a,x,b cxd]b c[axb c,x,d]d这个例子涉及到一个组 foo and bar.replace(/(.*?)/g, #$1#) #foo# and #bar#替换是一个函数 如果replacement是一个函数它会计算要替换匹配的字符串。此函数具有以下签名 function (completeMatch, group_1, ..., group_n, offset, inputStr)completeMatch与以前的$相同offset指示找到匹配的位置inputStr是正在匹配的内容。因此您可以使用特殊变量arguments来访问组通过arguments[1]访问第 1 组依此类推。例如 function replaceFunc(match) { return 2 * match }3 apples and 5 oranges.replace(/[0-9]/g, replaceFunc) 6 apples and 10 oranges标志/g的问题 设置了/g标志的正则表达式如果必须多次调用才能返回所有结果则存在问题。这适用于两种方法 RegExp.prototype.test() RegExp.prototype.exec() 然后 JavaScript 滥用正则表达式作为迭代器作为结果序列的指针。这会导致问题 问题 1无法内联/g正则表达式 例如 // Don’t do that: var count 0; while (/a/g.test(babaa)) count;前面的循环是无限的因为每次循环迭代都会创建一个新的正则表达式从而重新开始结果的迭代。因此必须重写代码 var count 0; var regex /a/g; while (regex.test(babaa)) count;这是另一个例子 // Don’t do that: function extractQuoted(str) {var match;var result [];while ((match /(.*?)/g.exec(str)) ! null) {result.push(match[1]);}return result; }调用前面的函数将再次导致无限循环。正确的版本是为什么lastIndex设置为 0 很快就会解释 var QUOTE_REGEX /(.*?)/g; function extractQuoted(str) {QUOTE_REGEX.lastIndex 0;var match;var result [];while ((match QUOTE_REGEX.exec(str)) ! null) {result.push(match[1]);}return result; }使用该函数 extractQuoted(hello, world) [ hello, world ]提示 最好的做法是不要内联然后您可以给正则表达式起一个描述性的名称。但是您必须意识到您不能这样做即使是在快速的 hack 中也不行。 问题 2/g正则表达式作为参数 调用test()和exec()多次的代码在作为参数传递给它的正则表达式时必须小心。它的标志/g必须激活并且为了安全起见它的lastIndex应该设置为零下一个示例中提供了解释。 问题 3共享的/g正则表达式例如常量 每当引用尚未新创建的正则表达式时您应该在将其用作迭代器之前将其lastIndex属性设置为零下一个示例中提供了解释。由于迭代取决于lastIndex因此这样的正则表达式不能同时在多个迭代中使用。 以下示例说明了问题 2。这是一个简单的实现函数用于计算字符串str中正则表达式regex的匹配次数 // Naive implementation function countOccurrences(regex, str) {var count 0;while (regex.test(str)) count;return count; }以下是使用此函数的示例 countOccurrences(/x/g, _x_x) 2第一个问题是如果正则表达式的/g标志未设置此函数将进入无限循环。例如 countOccurrences(/x/, _x_x) // never terminates第二个问题是如果regex.lastIndex不是 0函数将无法正确工作因为该属性指示从哪里开始搜索。例如 var regex /x/g;regex.lastIndex 2;countOccurrences(regex, _x_x) 1以下实现解决了两个问题 function countOccurrences(regex, str) {if (! regex.global) {throw new Error(Please set flag /g of regex);}var origLastIndex regex.lastIndex; // storeregex.lastIndex 0;var count 0;while (regex.test(str)) count;regex.lastIndex origLastIndex; // restorereturn count; }一个更简单的替代方法是使用match() function countOccurrences(regex, str) {if (! regex.global) {throw new Error(Please set flag /g of regex);}return (str.match(regex) || []).length; }有一个可能的陷阱如果设置了/g标志并且没有匹配项str.match()将返回null。在前面的代码中我们通过使用[]来避免这种陷阱如果match()的结果不是真值。 提示和技巧 本节提供了一些在 JavaScript 中使用正则表达式的技巧和窍门。 引用文本 有时当手动组装正则表达式时您希望逐字使用给定的字符串。这意味着不能解释任何特殊字符例如*[-所有这些字符都需要转义。JavaScript 没有内置的方法来进行这种引用但是您可以编写自己的函数quoteText它将按以下方式工作 console.log(quoteText(*All* (most?) aspects.)) \*All\* \(most\?\) aspects\.如果您需要进行多次搜索和替换则此函数特别方便。然后要搜索的值必须是设置了global标志的正则表达式。使用quoteText()您可以使用任意字符串。该函数如下所示 function quoteText(text) {return text.replace(/[\\^$.*?()[\]{}|!:-]/g, \\$); }所有特殊字符都被转义因为您可能希望在括号或方括号内引用多个字符。 陷阱没有断言例如^$正则表达式可以在任何地方找到 如果您不使用^和$等断言大多数正则表达式方法会在任何地方找到模式。例如 /aa/.test(xaay) true/^aa$/.test(xaay) false匹配一切或什么都不匹配 这是一个罕见的用例但有时您需要一个正则表达式它匹配一切或什么都不匹配。例如一个函数可能有一个用于过滤的正则表达式参数。如果该参数缺失您可以给它一个默认值一个匹配一切的正则表达式。 匹配一切 空正则表达式匹配一切。我们可以基于该正则表达式创建一个RegExp的实例就像这样 new RegExp().test(dfadsfdsa) truenew RegExp().test() true但是空正则表达式文字将是//这在 JavaScript 中被解释为注释。因此以下是通过文字获得的最接近的/(?:)/空的非捕获组。该组匹配一切同时不捕获任何内容这个组不会影响exec()返回的结果。即使 JavaScript 本身在显示空正则表达式时也使用前面的表示 new RegExp() /(?:)/匹配什么都不匹配 空正则表达式具有一个反义词——匹配什么都不匹配的正则表达式 var never /.^/;never.test(abc) falsenever.test() false手动实现后行断言 后行断言是一种断言。与先行断言类似模式用于检查输入中当前位置的某些内容但在其他情况下被忽略。与先行断言相反模式的匹配必须结束在当前位置而不是从当前位置开始。 以下函数将字符串NAME的每个出现替换为参数name的值但前提是该出现不是由引号引导的。我们通过“手动”检查当前匹配之前的字符来处理引号 function insertName(str, name) {return str.replace(/NAME/g,function (completeMatch, offset) {if (offset 0 ||(offset 0 str[offset-1] ! )) {return name;} else {return completeMatch;}}); }insertName(NAME NAME, Jane) Jane NAMEinsertName(NAME NAME, Jane) NAME Jane另一种方法是在正则表达式中包含可能转义的字符。然后您必须临时向您正在搜索的字符串添加前缀否则您将错过该字符串开头的匹配 function insertName(str, name) {var tmpPrefix ;str tmpPrefix str;str str.replace(/([^])NAME/g,function (completeMatch, prefix) {return prefix name;});return str.slice(tmpPrefix.length); // remove tmpPrefix }正则表达式速查表 原子参见原子一般 .点匹配除行终止符例如换行符之外的所有内容。使用[\s\S]来真正匹配一切。 字符类转义 \d匹配数字[0-9]\D匹配非数字[^0-9]。 \w匹配拉丁字母数字字符加下划线[A-Za-z0-9_]\W匹配所有其他字符。 \s匹配所有空白字符空格、制表符、换行符等\S匹配所有非空白字符。 字符类字符集[...]和[^...] 源字符[abc]除\ ] -之外的所有字符都与它们自身匹配 字符类转义参见前文[\d\w] 范围[A-Za-z0-9] 组 捕获组(...)反向引用\1 非捕获组(?:...) 量词参见量词 贪婪 ? * {n} {n,} {n,m} 勉强在任何贪婪量词后面加上?。 断言参见断言 输入的开始输入的结束^ $ 在词边界处不在词边界处\b \B 正向先行断言(?...)模式必须紧跟其后但在其他情况下被忽略 负向先行断言(?!...)模式不能紧跟其后但在其他情况下被忽略 分支| 创建正则表达式参见创建正则表达式 字面量/xyz/i在加载时编译 构造函数new RegExp(xzy, i)在运行时编译 标志参见标志 全局/g影响几种正则表达式方法 ignoreCase/i 多行/m^和$按行匹配而不是完整的输入 方法 regex.test(str): 是否有匹配参见RegExp.prototype.test: 是否有匹配 /g未设置是否有匹配 /g被设置返回与匹配次数相同的true。 str.search(regex): 有匹配项的索引是什么参见String.prototype.search: At What Index Is There a Match? regex.exec(str): 捕获组参见章节RegExp.prototype.exec: Capture Groups /g未设置仅捕获第一个匹配项的组仅调用一次 /g已设置捕获所有匹配项的组重复调用如果没有更多匹配项则返回null str.match(regex): 捕获组或返回所有匹配的子字符串参见String.prototype.match: Capture Groups or Return All Matching Substrings /g未设置捕获组 /g已设置返回数组中所有匹配的子字符串 str.replace(search, replacement): 搜索和替换参见String.prototype.replace: Search and Replace search字符串或正则表达式使用后者设置/g replacement字符串带有$1等或函数arguments[1]是第 1 组等返回一个字符串 有关使用标志/g的提示请参阅Problems with the Flag /g。 致谢 Mathias Bynensmathias和 Juan Ignacio Dopazojuandopazo建议使用match()和test()来计算出现次数Šime Vidassimevidas警告我在没有匹配项时要小心使用match()。全局标志导致无限循环的陷阱来自Andrea Giammarchi 的演讲webreflection。Claude Pache 告诉我在quoteText()中转义更多字符。 第二十章日期 原文20. Dates 译者飞龙 协议CC BY-NC-SA 4.0 JavaScript 的Date构造函数有助于解析、管理和显示日期。本章描述了它的工作原理。 日期 API 使用术语UTC协调世界时。在大多数情况下UTC 是 GMT格林尼治标准时间的同义词大致意味着伦敦英国的时区。 日期构造函数 有四种调用Date构造函数的方法 new Date(year, month, date?, hours?, minutes?, seconds?, milliseconds?) 从给定数据构造一个新的日期。时间相对于当前时区进行解释。Date.UTC()提供了类似的功能但是相对于 UTC。参数具有以下范围 year对于 0 ≤ year ≤ 99将添加 1900。 month0-110 是一月1 是二月依此类推 date1-31 hours0-23 minutes0-59 seconds0-59 milliseconds0-999 以下是一些示例 new Date(2001, 1, 27, 14, 55) Date {Tue Feb 27 2001 14:55:00 GMT0100 (CET)}new Date(01, 1, 27, 14, 55) Date {Wed Feb 27 1901 14:55:00 GMT0100 (CET)}顺便说一句JavaScript 继承了略微奇怪的约定将 0 解释为一月1 解释为二月依此类推这一点来自 Java。 new Date(dateTimeStr) 这是一个将日期时间字符串转换为数字的过程然后调用new Date(number)。日期时间格式解释了日期时间格式。例如 new Date(2004-08-29) Date {Sun Aug 29 2004 02:00:00 GMT0200 (CEST)}非法的日期时间字符串导致将NaN传递给new Date(number)。 new Date(timeValue) 根据自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数创建日期。例如 new Date(0) Date {Thu Jan 01 1970 01:00:00 GMT0100 (CET)}这个构造函数的反函数是getTime()方法它返回毫秒数 new Date(123).getTime() 123您可以使用NaN作为参数这将产生Date的一个特殊实例即“无效日期” var d new Date(NaN);d.toString() Invalid Dated.toJSON() nulld.getTime() NaNd.getYear() NaNnew Date() 创建当前日期和时间的对象它与new Date(Date.now())的作用相同。 日期构造函数方法 构造函数Date有以下方法 Date.now() 以毫秒为单位返回当前日期和时间自 1970 年 1 月 1 日 00:00:00 UTC 起。它产生与new Date().getTime()相同的结果。 Date.parse(dateTimeString) 将 dateTimeString 转换为自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。日期时间格式解释了 dateTimeString 的格式。结果可用于调用 new Date(number)。以下是一些示例 Date.parse(1970-01-01) 0Date.parse(1970-01-02) 86400000如果无法解析字符串此方法将返回 NaN Date.parse(abc) NaNDate.UTC(year, month, date?, hours?, minutes?, seconds?, milliseconds?) 将给定数据转换为自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。它与具有相同参数的 Date 构造函数有两种不同之处 它返回一个数字而不是一个新的日期对象。 它将参数解释为世界协调时间而不是本地时间。 日期原型方法 本节涵盖了 Date.prototype 的方法。 时间单位的获取器和设置器 时间单位的获取器和设置器可使用以下签名 本地时间 Date.prototype.get«Unit»() 返回根据本地时间的 Unit。 Date.prototype.set«Unit»(number) 根据本地时间设置 Unit。 世界协调时间 Date.prototype.getUTC«Unit»() 返回根据世界协调时间的 Unit。 Date.prototype.setUTC«Unit»(number) 根据世界协调时间设置 Unit。 Unit 是以下单词之一 FullYear通常是四位数 Month月份0-11 Date月份中的某一天1-31 Day仅获取器星期几0-60 代表星期日 Hours小时0-23 Minutes分钟0-59 Seconds秒0-59 Milliseconds毫秒0-999 例如 var d new Date(1968-11-25); Date {Mon Nov 25 1968 01:00:00 GMT0100 (CET)}d.getDate() 25d.getDay() 1各种获取器和设置器 以下方法使您能够获取和设置自 1970 年 1 月 1 日以来的毫秒数以及更多内容 Date.prototype.getTime() 返回自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数参见时间值日期作为自 1970-01-01 以来的毫秒数。 Date.prototype.setTime(timeValue) 根据自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数设置日期参见时间值日期作为自 1970-01-01 以来的毫秒数。 Date.prototype.valueOf() 与 getTime() 相同。当日期转换为数字时将调用此方法。 Date.prototype.getTimezoneOffset() 返回本地时间和世界协调时间之间的差异以分钟为单位。 Year 单位已被弃用推荐使用 FullYear Date.prototype.getYear() 已被弃用请改用 getFullYear()。 Date.prototype.setYear(number) 已被弃用请改用 setFullYear()。 将日期转换为字符串 请注意转换为字符串高度依赖于实现。以下日期用于计算以下示例中的输出在撰写本书时Firefox 的支持最完整 new Date(2001,9,30, 17,43,7, 856);时间可读 Date.prototype.toTimeString() 17:43:07 GMT0100 (CET)以当前时区的时间显示。 Date.prototype.toLocaleTimeString() 17:43:07以特定于区域的格式显示的时间。此方法由 ECMAScript 国际化 API参见ECMAScript 国际化 API提供并且如果没有它就没有太多意义。 日期可读 Date.prototype.toDateString() Tue Oct 30 2001日期。 Date.prototype.toLocaleDateString() 10/30/2001以特定于区域的格式显示的日期。此方法由 ECMAScript 国际化 API参见ECMAScript 国际化 API提供并且如果没有它就没有太多意义。 日期和时间可读 Date.prototype.toString() Tue Oct 30 2001 17:43:07 GMT0100 (CET)日期和时间以当前时区的时间。对于没有毫秒的任何 Date 实例即秒数已满以下表达式为真 Date.parse(d.toString()) d.valueOf() Date.prototype.toLocaleString()jsTue Oct 30 17:43:07 2001以区域特定格式的日期和时间。此方法由 ECMAScript 国际化 API 提供请参见[ECMAScript 国际化 API](ch30.html#i18n_api ECMAScript 国际化 API)如果没有它这个方法就没有太多意义。 Date.prototype.toUTCString()jsTue, 30 Oct 2001 16:43:07 GMT日期和时间使用 UTC。 Date.prototype.toGMTString()已弃用请改用toUTCString()。日期和时间机器可读 Date.prototype.toISOString()js2001-10-30T16:43:07.856Z所有内部属性都显示在返回的字符串中。格式符合[日期时间格式](ch20.html#date_time_formats 日期时间格式)时区始终为Z。 Date.prototype.toJSON()此方法内部调用toISOString()。它被JSON.stringify()参见[JSON.stringify(value, replacer?, space?)](ch22.html#JSON.stringify JSON.stringify(value, replacer?, space?))用于将日期对象转换为 JSON 字符串。## 日期时间格式本节描述了以字符串形式表示时间点的格式。有许多方法可以这样做仅指示日期包括一天中的时间省略时区指定时区等。在支持日期时间格式方面ECMAScript 5 紧密遵循标准 ISO 8601 扩展格式。JavaScript 引擎相对完全地实现了 ECMAScript 规范但仍然存在一些变化因此您必须保持警惕。最长的日期时间格式是js YYYY-MM-DDTHH:mm:ss.sssZ每个部分代表日期时间数据的几个十进制数字。例如YYYY表示格式以四位数年份开头。以下各小节解释了每个部分的含义。这些格式与以下方法相关 Date.parse() 可以解析这些格式。 new Date()可以解析这些格式。 Date.prototype.toISOString()创建了上述“完整”格式的字符串 new Date().toISOString() 2014-09-12T23:05:07.414Z日期格式无时间 以下日期格式可用 YYYY-MM-DD YYYY-MM YYYY它们包括以下部分 YYYY 指的是年份公历。 MM指的是月份从 01 到 12。 DD 指的是日期从 01 到 31。 例如 new Date(2001-02-22) Date {Thu Feb 22 2001 01:00:00 GMT0100 (CET)}时间格式无日期 以下时间格式可用。如您所见时区信息Z是可选的 THH:mm:ss.sss THH:mm:ss.sssZTHH:mm:ss THH:mm:ssZTHH:mm THH:mmZ它们包括以下部分 T是格式时间部分的前缀字面上的T而不是数字。 HH指的是小时从 00 到 23。您可以使用 24 作为HH的值指的是第二天的 00 小时但是接下来的所有部分都必须为 0。 mm 表示分钟从 00 到 59。 ss 表示秒从 00 到 59。 sss 表示毫秒从 000 到 999。 Z指的是时区以下两者之一 “Z”表示 UTC “”或“-”后跟时间“hh:mm” 一些 JavaScript 引擎允许您仅指定时间其他需要日期 new Date(T13:17) Date {Thu Jan 01 1970 13:17:00 GMT0100 (CET)}日期时间格式 日期格式和时间格式也可以结合使用。在日期时间格式中您可以使用日期或日期和时间或在某些引擎中仅使用时间。例如 new Date(2001-02-22T13:17) Date {Thu Feb 22 2001 13:17:00 GMT0100 (CET)}时间值自 1970-01-01 以来的毫秒数 日期 API 称之为time的东西在 ECMAScript 规范中被称为时间值。它是一个原始数字以自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数编码日期。每个日期对象都将其状态存储为时间值在内部属性[[PrimitiveValue]]中与包装构造函数BooleanNumber和String的实例用于存储其包装的原始值的相同属性。 警告 时间值中忽略了闰秒。 以下方法适用于时间值 new Date(timeValue) 使用时间值创建日期。 Date.parse(dateTimeString)解析带有日期时间字符串的字符串并返回时间值。 Date.now()返回当前日期时间作为时间值。 Date.UTC(year, month, date?, hours?, minutes?, seconds?, milliseconds?) 解释参数相对于 UTC 并返回时间值。 Date.prototype.getTime() 返回接收器中存储的时间值。 Date.prototype.setTime(timeValue)根据指定的时间值更改日期。 Date.prototype.valueOf()返回接收者中存储的时间值。该方法确定了如何将日期转换为原始值如下一小节所述。 JavaScript 整数的范围53 位加上一个符号足够大可以表示从 1970 年前约 285,616 年开始到 1970 年后约 285,616 年结束的时间跨度。 以下是将日期转换为时间值的几个示例 new Date(1970-01-01).getTime() 0new Date(1970-01-02).getTime() 86400000new Date(1960-01-02).getTime() -315532800000Date构造函数使您能够将时间值转换为日期 new Date(0) Date {Thu Jan 01 1970 01:00:00 GMT0100 (CET)}new Date(24 * 60 * 60 * 1000) // 1 day in ms Date {Fri Jan 02 1970 01:00:00 GMT0100 (CET)}new Date(-315532800000) Date {Sat Jan 02 1960 01:00:00 GMT0100 (CET)}将日期转换为数字 通过Date.prototype.valueOf()将日期转换为数字返回一个时间值。这使您能够比较日期 new Date(1980-05-21) new Date(1980-05-20) true您也可以进行算术运算但要注意闰秒被忽略 new Date(1980-05-21) - new Date(1980-05-20) 86400000警告 使用加号运算符将日期加到另一个日期或数字会得到一个字符串因为将日期转换为原始值的默认方式是将日期转换为字符串请参阅加号运算符了解加号运算符的工作原理 new Date(2024-10-03) 86400000 Thu Oct 03 2024 02:00:00 GMT0200 (CEST)86400000new Date(Number(new Date(2024-10-03)) 86400000) Fri Oct 04 2024 02:00:00 GMT0200 (CEST)
http://www.zqtcl.cn/news/698222/

相关文章:

  • 广州哪里有做网站推广最牛的网站建
  • 建设网站用户名是什么原因世界500强企业排名2020
  • 创建网站要找谁手机网站后台源码
  • canvas网站源码网站静态和动态区别
  • 网站建设需要了解哪些方面数据分析工具
  • 求个网站没封的2021网站建设初步课程介绍
  • 沈阳网站前端网站建栏目建那些
  • 经典网站案例江苏省建设厅官网
  • 公司建设网站需要多少钱重庆房产网站建设
  • 鹤岗市建设局网站可信网站认证有用吗
  • 网站注册的账号怎么注销如何百度推广
  • 用wordpress制作网站模板阿里云网站建设合作
  • 金华建设公司网站宝武马钢集团公司招聘网站
  • 万州网站制作公司阳江市网站建设
  • 下载建设网站软件投资公司注册资金多少
  • 如何创建一个论坛网站免费域名解析平台
  • 国外经典手机网站设计单位做网站有哪些
  • 网站备案 优帮云百度提交入口网址截图
  • 广州五羊建设官方网站富阳区住房和城乡建设局网站
  • 网站代理怎么做的wordpress有什么缺点
  • 哪些网站可以做免费外贸Wordpress首图自动切换
  • 建网站几个按钮公司黄页企业名录在哪里查
  • 网站建设类外文翻译游戏开科技软件免费
  • 黄山家居网站建设怎么样济南在线制作网站
  • 东莞电子产品网站建设营销型网站推广方式的论文
  • 如何寻找做网站的客户聚名网查询
  • 甘肃制作网站凡科快图官网登录入口在线
  • discuz网站建设教学视频教程哪些大型网站有做互联网金融
  • jquery动画特效网站物流网站前端模板下载
  • 上海集团网站建设网站都是用什么语言写的