一、深入 JavaScript 核心

你可能已经拥有 iPhone 多年了,并且认为自己是一个有经验的用户。同时,您可以通过按 delete 键在键入时一次删除一个不需要的字符。然而,有一天你发现快速摇动可以让你在一次点击中删除整个信息。然后你想知道为什么你之前不知道这一点。编程也会发生同样的事情。我们可以对自己的编码感到非常满意,直到我们突然遇到一个技巧或一个鲜为人知的语言特性,使我们重新考虑多年来所做的全部工作。事实证明,我们可以用更干净、更可读、更可测试和更可维护的方式来实现这一点。因此,我们假设您已经有了使用 JavaScript 的经验;但是,本章为您提供了改进代码的最佳实践。我们将讨论以下主题:

  • 使您的代码具有可读性和表达能力
  • 掌握 JavaScript 中的多行字符串
  • 以 ES5 方式操作数组
  • 以优雅、可靠、安全和快速的方式遍历对象
  • 声明对象的最有效方法
  • 如何在 JavaScript 中创建魔术方法

使您的代码具有可读性和表达力

为了使代码更具可读性、表达性和简洁性,有许多实践和启发法。稍后我们将讨论这个主题,但这里我们将讨论语法糖。该术语表示使代码更具表达力和可读性的另一种语法。事实上,我们从一开始就在 JavaScript 中使用了一些。例如,从 C.foo++继承的递增/递减和加减赋值运算符是foo=foo+1的语法糖,foo+=barfoo=foo+bar的较短形式。除此之外,我们还有一些技巧可以达到同样的目的。

JavaScript 将逻辑表达式应用于所谓的短路评估。这意味着从左到右读取表达式,但一旦在早期阶段确定条件结果,就不会计算表达式尾部。如果我们有假,解释器将从第一次测试中知道结果为真,而不管其他测试如何。所以false部分没有被评估,这为创造力开辟了一条道路。

函数参数默认值

当我们需要为参数指定默认值时,我们可以这样做:

function stub( foo ) {
 return foo || "Default value";
}

console.log( stub( "My value" ) ); // My value
console.log( stub() ); // Default value

这里发生了什么事?当footruenot undefinedNaNnullfalse0""时,逻辑表达式的结果为foo,否则对表达式求值直到Default value,这是最终结果。

从第 6 版 EcmaScript(JavaScript 语言规范)开始,我们可以使用更好的语法:

function stub( foo = "Default value" ) {
 return foo;
}

条件调用

在编写代码时,我们会根据以下条件缩短代码:

var age = 20;
age >= 18 && console.log( "You are allowed to play this game" );
age >= 18 || console.log( "The game is restricted to 18 and over" );

在前面的示例中,如果左侧条件真实,我们使用 AND(&&操作符调用console.log。OR(||运算符的作用相反,如果条件为Falsy,则调用console.log

我认为在实践中,最常见的情况是仅在提供函数时才调用该函数的速记条件:

/**
* @param {Function} [cb] - callback
*/
function fn( cb ) {
 cb && cb();
};

下面是关于这一点的又一个例子:

/**
* @class AbstractFoo
*/
AbstractFoo = function(){
 // call this.init if the subclass has init method
 this.init && this.init();
};

只有随着 CoffeeScript 的发展,语法糖才被全面引入 JavaScript 世界。CoffeeScript 是将源代码编译成 JavaScript 的语言的一个子集。实际上,受 Ruby、Python 和 Haskell 启发的 CoffeeScript 为 JavaScript 开发人员解锁了箭头函数、扩展和其他语法。2011 年,BrendanEich(JavaScript 的作者)承认 CoffeeScript 影响了他在 EcmaScript Harmony 上的工作,这项工作于今年夏天在 ECMA-262 第 6 版规范中完成。从营销角度来看,规范编写者同意使用新的名称约定,将第 6 版命名为 EcmaScript 2015,将第 7 版命名为 EcmaScript 2016。然而,社区习惯于使用缩写词,如 ES6 和 ES7。为了避免在本书中进一步混淆,我们将通过这些名称参考规范。现在我们可以看看这是如何影响新的 JavaScript 的。

箭头功能

传统函数表达式可能如下:

function( param1, param2 ){ /* function body */ }

当使用 arrow 函数(也称为 fat arrow 函数)语法声明表达式时,我们将以不太详细的形式进行声明,如下所示:

( param1, param2 ) => { /* function body */ }

在我看来,我们并没有从中获得多少好处。但是如果我们需要,比方说,数组方法回调,传统的形式如下:

function( param1, param2 ){ return expression; }

现在,等效箭头函数变短,如下所示:

( param1, param2 ) => expression

我们可以通过以下方式在阵列中进行过滤:

// filter all the array elements greater than 2
var res = [ 1, 2, 3, 4 ].filter(function( v ){
 return v > 2;
})
console.log( res ); // [3,4]

使用数组函数,我们可以以更简洁的形式进行过滤:

var res  = [ 1, 2, 3, 4 ].filter( v => v > 2 );
console.log( res ); // [3,4]

除了较短的函数声明语法外,箭头函数还带来了所谓的词法this。它不创建自己的上下文,而是使用周围对象的上下文,如下所示:

"use strict";
/**
* @class View
*/   
let View = function(){
 let button = document.querySelector( "[data-bind=\"btn\"]" );
 /**
  * Handle button clicked event
  * @private 
  */
 this.onClick = function(){
   console.log( "Button clicked" );
 };
 button.addEventListener( "click", () => {
   // we can safely refer surrounding object members
   this.onClick(); 
 }, false );
}

在前面的示例中,我们为 DOM 事件(click)订阅了一个处理函数。在处理程序的范围内,我们仍然可以访问视图上下文(this,因此我们不需要将处理程序绑定到外部范围或通过闭包将其作为变量传递:

var that = this;
button.addEventListener( "click", function(){
  // cross-cutting concerns
  that.onClick(); 
}, false );

方法定义

正如前面的部分所提到的,当声明小型内联回调时,箭头函数非常方便,但始终将其应用于较短的语法是有争议的。但是,ES6 提供了除箭头函数之外的新的替代方法定义语法。旧学校方法声明可能如下所示:

var foo = {
 bar: function( param1, param2 ) {
 }
}

在 ES6 中,我们可以去掉函数关键字和冒号。所以前面的代码可以这样说:

let foo = {
 bar ( param1, param2 ) {
 }
}

休息操作员

另一个从 CoffeeScript 借用的语法结构变成了 JavaScript 作为 rest 操作符(尽管这种方法在 CoffeeScript 中称为splats

当我们有一些必需的函数参数和未知数量的 rest 参数时,我们通常会这样做:

"use strict";
var cb = function() {
 // all available parameters into an array
 var args = [].slice.call( arguments ),
     // the first array element to foo and shift
     foo = args.shift(),
     // the new first array element to bar and shift
     bar = args.shift();
 console.log( foo, bar, args );
};
cb( "foo", "bar", 1, 2, 3 ); // foo bar [1, 2, 3]

现在,请查看此代码在 ES6 中的表现力:

let cb = function( foo, bar, ...args ) {
 console.log( foo, bar, args );
}
cb( "foo", "bar", 1, 2, 3 ); // foo bar [1, 2, 3]

函数参数不是 rest 运算符的唯一应用。例如,我们也可以在破坏中使用它,如下所示:

let [ bar, ...others ] = [ "bar", "foo", "baz", "qux" ];
console.log([ bar, others ]); // ["bar",["foo","baz","qux"]]

排列操作员

类似地,我们可以将数组元素扩展为参数:

let args = [ 2015, 6, 17 ],
   relDate = new Date( ...args );
console.log( relDate.toString() );  // Fri Jul 17 2015 00:00:00 GMT+0200 (CEST)

ES6 还为对象创建和继承提供了表达语法糖,但我们稍后将在声明对象的最有效方式一节中对此进行研究。

掌握 JavaScript 中的多行字符串

多行字符串不是 JavaScript 的好组成部分。虽然它们很容易用其他语言(例如 NOWDOC)声明,但不能在多行中保留单引号或双引号字符串。这将导致语法错误,因为 JavaScript 中的每一行都被视为可能的命令。您可以设置反斜杠以显示您的意图:

var str = "Lorem ipsum dolor sit amet, \n\
consectetur adipiscing elit. Nunc ornare, \n\
diam ultricies vehicula aliquam, mauris \n\
ipsum dapibus dolor, quis fringilla leo ligula non neque";

这类作品。然而,一旦遗漏了尾随空格,就会出现语法错误,这是不容易发现的。虽然大多数脚本代理都支持这种语法,但它不是 EcmaScript 规范的一部分。

EcmaScript for XMLE4X的时代,我们可以将一个纯 XML 分配给一个字符串,这为如下声明开辟了一条道路:

var str = <>Lorem ipsum dolor sit amet, 
consectetur adipiscing 
elit. Nunc ornare </>.toString();

如今 E4X 已被弃用,不再受支持。

串联与数组连接

我们也可以使用字符串连接。它可能感觉笨拙,但它是安全的:

var str = "Lorem ipsum dolor sit amet, \n" +
 "consectetur adipiscing elit. Nunc ornare,\n" +
 "diam ultricies vehicula aliquam, mauris \n" +
 "ipsum dapibus dolor, quis fringilla leo ligula non neque";

您可能会感到惊讶,但连接比数组连接要慢。因此,以下技术将工作得更快:

var str = [ "Lorem ipsum dolor sit amet, \n",
 "consectetur adipiscing elit. Nunc ornare,\n",
 "diam ultricies vehicula aliquam, mauris \n",
 "ipsum dapibus dolor, quis fringilla leo ligula non neque"].join( "" );

模板文字

ES6 呢?最新的 EcmaScript 规范引入了一种新的字符串文字,即模板文字:

var str = `Lorem ipsum dolor sit amet, \n
consectetur adipiscing elit. Nunc ornare, \n
diam ultricies vehicula aliquam, mauris \n
ipsum dapibus dolor, quis fringilla leo ligula non neque`;

现在语法看起来很优雅。但还有更多。模板文字真的让我们想起了 NOWDOC。您可以在字符串中引用范围中声明的任何变量:

"use strict";
var title = "Some title",
   text = "Some text",
   str = `<div class="message">
<h2>${title}</h2>
<article>${text}</article>
</div>`;
console.log( str );

结果如下:

<div class="message">
<h2>Some title</h2>
<article>Some text</article>
</div>

如果你想知道什么时候可以安全地使用这个语法,我有一个好消息告诉你,这个功能已经被(几乎)所有主要的脚本代理(支持了 http://kangax.github.io/compat-table/es6/ )。

通过传送带的多线串

随着 ReactJS 的发展,Facebook 的 EcmaScript 语言扩展名为 JSX(https://facebook.github.io/jsx/ )现在真的越来越有动力了。显然受到前面提到的 E4X 的影响,他们为类似 XML 的内容提出了一种字符串文本,根本不需要任何筛选。此类型支持与 ES6 模板类似的模板插值:

"use strict";
var Hello = React.createClass({
 render: function() {
 return <div class="message">
<h2>{this.props.title}</h2>
<article>{this.props.text}</article>
</div>;
 }
});

React.render(<Hello title="Some title" text="Some text" />, node);

另一种声明多行字符串的方法是使用 CommonJS 编译器(http://dsheiko.github.io/cjsc/ )。在解析“require”依赖项时,编译器将非.js/.json内容的任何内容转换为单行字符串:

foo.txt

Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Nunc ornare,
diam ultricies vehicula aliquam, mauris
ipsum dapibus dolor, quis fringilla leo ligula non neque

consumer.js

var str = require( "./foo.txt" );
console.log( str );

您可以在第 6 章大型 JavaScript 应用架构中找到 JSX 使用的示例。

以 ES5 方式操作阵列

几年前,当 ES5 功能的支持较差时(EcmaScript 第 5 版于 2009 年定稿),下划线和 Lo Dash 等库非常受欢迎,因为它们提供了一套全面的实用程序来处理数组/集合。今天,许多开发人员仍然使用第三方库(包括 jQuery/Zepro)来实现方法,如mapfiltereverysomereduceindexOf,而这些都是以 JavaScript 的本机形式提供的。这仍然取决于您如何使用这些库,但很可能您不再需要它们。让我们看看 JavaScript 中现在有什么。

ES5 中的数组方法

Array.prototype.forEach可能是阵列中最常用的方法。也就是说,是_.each的本机实现,或者例如$.each实用程序的本机实现。作为参数,forEach需要一个iteratee回调函数和一个您希望在其中执行回调的上下文(可选)。它将元素值、索引和整个数组传递给回调函数。大多数数组操作方法都使用相同的参数语法。请注意,jQuery 的$.each具有反向回调参数顺序:

"use strict";
var data = [ "bar", "foo", "baz", "qux" ];
data.forEach(function( val, inx ){
  console.log( val, inx ); 
});

Array.prototype.map通过变换给定数组的元素生成新数组:

"use strict";
var data = { bar: "bar bar", foo: "foo foo" },
   // convert key-value array into url-encoded string
   urlEncStr = Object.keys( data ).map(function( key ){
     return key + "=" + window.encodeURIComponent( data[ key ] );
   }).join( "&" );

console.log( urlEncStr ); // bar=bar%20bar&foo=foo%20foo

Array.prototype.filter返回一个数组,该数组由满足回调条件的给定数组值组成:

"use strict";
var data = [ "bar", "foo", "", 0 ],
   // remove all falsy elements
   filtered = data.filter(function( item ){
     return !!item;
   });

console.log( filtered ); // ["bar", "foo"]

Array.prototype.reduce/Array.prototype.reduceRight检索数组中值的乘积。该方法需要回调函数和可选的初始值作为参数。回调函数接收四个参数:累计值、当前值、索引和原始数组。例如,我们可以将累计值乘以当前值(返回 acc+=cur;),从而得到数组值之和。

除了使用这些方法进行计算外,我们还可以连接字符串值或数组:

"use strict";
var data = [[ 0, 1 ], [ 2, 3 ], [ 4, 5 ]],
   arr = data.reduce(function( prev, cur ) {
     return prev.concat( cur );
   }),
   arrReverse = data.reduceRight(function( prev, cur ) {
     return prev.concat( cur );
   });

console.log( arr ); //  [0, 1, 2, 3, 4, 5]
console.log( arrReverse ); // [4, 5, 2, 3, 0, 1]

Array.prototype.some测试给定数组的任何(或部分)值是否满足回调条件:

"use strict";
var bar = [ "bar", "baz", "qux" ],
   foo = [ "foo", "baz", "qux" ],
   /**
    * Check if a given context (this) contains the value
    * @param {*} val
    * @return {Boolean}
    */
   compare = function( val ){
     return this.indexOf( val ) !== -1; 
   };

console.log( bar.some( compare, foo ) ); // true

在本例中,我们检查了foo数组中是否有任何条形数组值可用。对于可测试性,我们需要将foo数组的引用传递到回调中。这里我们将其作为上下文注入。如果我们需要传递更多的引用,我们会将它们推送到键值对象中。

正如您可能注意到的,我们在本例中使用了Array.prototype.indexOf。方法与String.prototype.indexOf相同。这将返回找到的匹配项的索引或-1

Array.prototype.every测试给定数组的每个值是否满足回调条件:

"use strict";
var bar = [ "bar", "baz" ],
   foo = [ "bar", "baz", "qux" ],
   /**
    * Check if a given context (this) contains the value
    * @param {*} val
    * @return {Boolean}
    */
   compare = function( val ){
     return this.indexOf( val ) !== -1; 
   };

console.log( bar.every( compare, foo ) ); // true

如果您仍然担心在像 IE6-7 这样的旧浏览器中支持这些方法,您可以简单地用填充它们 https://github.com/es-shims/es5-shim

ES6 中的数组方法

在 ES6 中,我们只获得了一些新的方法,这些方法看起来更像是现有功能的捷径。

Array.prototype.fill使用给定值填充数组,如下所示:

"use strict";
var data = Array( 5 );
console.log( data.fill( "bar" ) ); // ["bar", "bar", "bar", "bar", "bar"]

Array.prototype.includes显式检查数组中是否存在给定值。那么它和arr.indexOf( val ) !== -1一样,如下图:

"use strict";
var data = [ "bar", "foo", "baz", "qux" ];
console.log( data.includes( "foo" ) );

Array.prototype.find过滤出与回调条件匹配的单个值。同样,这是我们可以通过Array.prototype.filter得到的。唯一的区别是 filter 方法返回数组或空值。在这种情况下,将返回一个单元素数组,如下所示:

"use strict";
var data = [ "bar", "fo", "baz", "qux" ],
   match = function( val ){
     return val.length < 3;
   };
console.log( data.find( match ) ); // fo

以优雅、可靠、安全、快速的方式穿越物体

当有一个键值对象(比方说选项)并且需要迭代它时,这是一种常见的情况。有一种学术方法可以做到这一点,如下代码所示:

"use strict";
var options = {
    bar: "bar",
    foo: "foo"
   },
   key;
for( key in options ) {
 console.log( key, options[ key] );
}

上述代码输出以下内容:

bar bar
foo foo

现在让我们设想一下,您在文档中加载的任何第三方库都会增强内置的Object

Object.prototype.baz = "baz";

现在,当我们运行示例代码时,我们将得到一个额外的不需要的条目:

bar bar
foo foo
baz baz

这个问题的解决方案是众所周知的,我们必须用Object.prototype.hasOwnProperty方法测试钥匙:

//…
for( key in options ) {
 if ( options.hasOwnProperty( key ) ) {
   console.log( key, options[ key] );
 }
}

安全快速地迭代键值对象

让我们面对事实,结构很笨拙,需要优化(我们必须对每个给定的密钥执行hasOwnProperty测试)。幸运的是,JavaScript 有Object.keys方法可以检索所有可枚举自身(非继承)属性的所有字符串值键。这将为我们提供所需的键作为一个数组,我们可以使用Array.prototype.forEach进行迭代:

"use strict";
var options = {
    bar: "bar",
    foo: "foo"
   };
Object.keys( options ).forEach(function( key ){
 console.log( key, options[ key] );
});

除了优雅之外,我们通过这种方式获得了更好的性能。为了了解我们的收益,您可以在不同的浏览器中运行此在线测试,例如:http://codepen.io/dsheiko/pen/JdrqXa

枚举类似数组的对象

argumentsnodeListnode.querySelectorAlldocument.forms这样的对象看起来像数组,实际上它们不是。与数组类似,它们具有length属性,可以在for循环中迭代。以对象的形式,它们可以像我们之前研究的那样被遍历。但是他们没有任何数组操作方法(forEachmapfiltersome等等)。问题是我们可以很容易地将它们转换为数组,如下所示:

"use strict";
var nodes = document.querySelectorAll( "div" ),
   arr = Array.prototype.slice.call( nodes );

arr.forEach(function(i){
 console.log(i);
});

前面的代码可以更短:

arr = [].slice.call( nodes )

这是一个非常方便的解决方案,但看起来像一个骗局。在 ES6 中,我们可以使用专用方法进行相同的转换:

arr = Array.from( nodes );

ES6 系列

ES6 引入了一种新类型的对象 iterable objects。这些对象的元素可以一次检索一个。它们与其他语言中的迭代器完全相同。除了数组之外,JavaScript 还收到了两个新的可移植数据结构,SetMapSet这是一组独特的值:

"use strict";
let foo = new Set();
foo.add( 1 );
foo.add( 1 );
foo.add( 2 );
console.log( Array.from( foo ) ); // [ 1, 2 ]

let foo = new Set(), 
   bar = function(){ return "bar"; };
foo.add( bar );
console.log( foo.has( bar ) ); // true

贴图类似于 key-value 对象,但可能具有任意键值。这就不同了。假设我们需要编写一个元素包装器来提供类似 jQuery 的事件 API。通过使用on方法,我们不仅可以传递处理程序回调函数,还可以传递上下文(this。我们将给定的回调绑定到cb.bind( context )上下文。这意味着addEventListener接收到与回调不同的函数引用。那我们怎么退订处理程序呢?我们可以通过一个由事件名称和callback函数引用组成的键将新引用存储在Map中:

"use strict";
/**
* @class
* @param {Node} el
*/
let El = function( el ){
 this.el = el;
 this.map = new Map();
};
/**
* Subscribe a handler on event
* @param {String} event
* @param {Function} cb
* @param {Object} context
*/
El.prototype.on = function( event, cb, context ){
 let handler = cb.bind( context || this );
 this.map.set( [ event, cb ], handler );
 this.el.addEventListener( event, handler, false );
};
/**
* Unsubscribe a handler on event
* @param {String} event
* @param {Function} cb
*/

El.prototype.off = function( event, cb ){
 let handler = cb.bind( context ),
     key = [ event, handler ];
 if ( this.map.has( key ) ) {
 this.el.removeEventListener( event, this.map.get( key ) );
 this.map.delete( key );
 }
};

任何 iterable 对象都有方法keysvaluesentries,其中键的工作方式与Object.keys相同,其他键分别返回数组值和键值对数组。现在让我们看看如何遍历 iterable 对象:

"use strict";
let map = new Map()
 .set( "bar", "bar" )
 .set( "foo", "foo" ),
   pair;
for ( pair of map ) {
 console.log( pair );
}

// OR 
let map = new Map([
   [ "bar", "bar" ],
   [ "foo", "foo" ],
]);
map.forEach(function( value, key ){
 console.log( key, value );
});

Iterable 对象有数组等操作方法。所以我们可以使用forEach。此外,它们可以通过for...infor...of循环进行迭代。第一个检索索引,第二个检索值。

声明对象的最有效方式

我们如何在 JavaScript 中声明对象?如果我们需要一个名称空间,我们可以简单地使用对象文本。但是当我们需要一个对象类型时,我们需要三思而后行,因为它会影响我们面向对象代码的可维护性。

经典方法

我们可以创建一个构造函数并将成员链接到其上下文:

"use strict"; 
/**
 * @class
 */
var Constructor = function(){
   /**
   * @type {String}
   * @public
   */
   this.bar = "bar";
   /**
   * @public
   * @returns {String}
   */
   this.foo = function() {
    return this.bar;
   };
 },
 /** @type Constructor */
 instance = new Constructor();

console.log( instance.foo() ); // bar
console.log( instance instanceof Constructor ); // true

我们还可以将成员分配给构造函数原型。结果如下所示:

"use strict";
/**
* @class
*/
var Constructor = function(){},
   instance;
/**
* @type {String}
* @public
*/
Constructor.prototype.bar = "bar";
/**
* @public
* @returns {String}
*/
Constructor.prototype.foo = function() {
 return this.bar;
};
/** @type Constructor */
instance = new Constructor();

console.log( instance.foo() ); // bar
console.log( instance instanceof Constructor ); // true

在第一种情况下,构造函数函数体内的对象结构与构造逻辑混合在一起。在第二种情况下,通过重复Constructor.prototype,我们违反了不要重复自己干燥原则。

与私人国家的接触

那我们怎么能不这样做呢?我们可以通过构造函数返回对象文字:

"use strict";
/**
 * @class
 */
var Constructor = function(){
     /**
     * @type {String}
     * @private
     */
     var baz = "baz";
     return {
       /**
       * @type {String}
       * @public
       */
       bar: "bar",
       /**
       * @public
       * @returns {String}
       */
       foo: function() {
        return this.bar + " " + baz;
       }
     };
   },
   /** @type Constructor */
   instance = new Constructor();

console.log( instance.foo() ); // bar baz
console.log( instance.hasOwnProperty( "baz") ); // false
console.log( Constructor.prototype.hasOwnProperty( "baz") ); // false
console.log( instance instanceof Constructor ); // false

这种方法的优点是,在构造函数范围内声明的任何变量都与返回的对象位于同一个闭包中,因此可以通过对象使用。我们可以考虑这样的变量:私人成员。坏消息是我们将失去构造函数原型。当构造函数在实例化过程中返回一个对象时,这个对象成为一个全新表达式的结果。

与原型链的继承

继承权呢?经典的方法是使子类型原型成为超类型的实例:

"use strict";
 /**
 * @class
 */
var SuperType = function(){
       /**
       * @type {String}
       * @public
       */
       this.foo = "foo";
     },
     /**
      * @class
      */
     Constructor = function(){
       /**
       * @type {String}
       * @public
       */
       this.bar = "bar";
     },
     /** @type Constructor */
     instance;

 Constructor.prototype = new SuperType();
 Constructor.prototype.constructor = Constructor;

 instance = new Constructor();
 console.log( instance.bar ); // bar
 console.log( instance.foo ); // foo
 console.log( instance instanceof Constructor ); // true
 console.log( instance instanceof SuperType ); // true  

您可能会遇到一些代码,其中使用Object.create而不是新运算符进行实例化。在这里你必须知道两者之间的区别。Object.create将一个对象作为参数,并以传递的对象作为原型创建一个新对象。在某些方面,这让我们想起了克隆。检查这一点,您将声明一个对象文字(proto),并在第一个对象的基础上创建一个新的对象(实例),并使用Object.create。无论您现在对新创建的对象做什么更改,它们都不会反映在原始对象(proto)上。但是,如果您更改原始属性,您将发现其派生属性中的属性发生了更改(实例):

"use strict";
var proto = {
 bar: "bar",
 foo: "foo"
}, 
instance = Object.create( proto );
proto.bar = "qux",
instance.foo = "baz";
console.log( instance ); // { foo="baz",  bar="qux"}
console.log( proto ); // { bar="qux",  foo="foo"}

使用 Object.create 从原型继承

与新的操作符不同,Object.create不调用构造函数。因此,当我们使用它来填充子类型原型时,我们正在丢失位于supertype构造函数中的所有逻辑。这样,就不会调用supertype构造函数:

// ...
SuperType.prototype.baz = "baz";
Constructor.prototype = Object.create( SuperType.prototype );
Constructor.prototype.constructor = Constructor;

instance = new Constructor();

console.log( instance.bar ); // bar
console.log( instance.baz ); // baz
console.log( instance.hasOwnProperty( "foo" ) ); // false
console.log( instance instanceof Constructor ); // true
console.log( instance instanceof SuperType ); // true

使用 Object.assign 从原型继承

在寻找最优结构时,我希望通过对象文本声明成员,但仍然有到原型的链接。许多第三方项目利用自定义函数(扩展)将结构对象文本合并到构造函数原型中。实际上,ES6 提供了一种Object.assign本地方法。我们可以按如下方式使用它:

"use strict";
   /**
    * @class
    */
var SuperType = function(){
     /**
     * @type {String}
     * @public
     */
     this.foo = "foo";
   },
   /**
    * @class
    */
   Constructor = function(){
     /**
     * @type {String}
     * @public
     */
     this.bar = "bar";
   },
   /** @type Constructor */
   instance;

Object.assign( Constructor.prototype = new SuperType(), {
 baz: "baz"
});
instance = new Constructor();
console.log( instance.bar ); // bar
console.log( instance.foo ); // foo
console.log( instance.baz ); // baz
console.log( instance instanceof Constructor ); // true
console.log( instance instanceof SuperType ); // true

这看起来几乎符合要求,只是有一点不便。Object.assign简单地将源对象的值分配给目标对象,而不考虑其类型。因此,如果您有一个对象的源属性(例如,ObjectArray实例),那么目标对象将接收一个引用而不是一个值。因此,您必须在初始化期间手动重置任何对象属性。

引伸式进近

Simon Boudrias 提出的 ExtendClass 是一个看似完美的解决方案(https://github.com/SBoudrias/class-extend )。他的小库使用扩展静态方法公开了Base构造函数。我们使用此方法扩展此伪类及其任何衍生物:

"use strict";
   /**
    * @class
    */
var SuperType = Base.extend({
     /**
      * @pulic
      * @returns {String}
      */
     foo: function(){ return "foo public"; },
     /**
      * @constructs SuperType
      */
     constructor: function () {}
   }),
   /**
    * @class
    */
   Constructor = SuperType.extend({
     /**
      * @pulic
      * @returns {String}
      */      
     bar: function(){ return "bar public"; }
   }, {
     /**
      * @static
      * @returns {String}
      */      
     bar: function(){ return "bar static"; }
   }),
   /** @type Constructor */
   instance = new Constructor();

console.log( instance.foo() ); // foo public
console.log( instance.bar() ); // bar public
console.log( Constructor.bar() ); // bar static
console.log( instance instanceof Constructor ); // true
console.log( instance instanceof SuperType ); // true

ES6 课程

TC39(EcmaScript 工作组)非常清楚这个问题,因此新的语言规范提供了额外的语法来构造对象类型:

"use strict";
class AbstractClass {
 constructor() {
   this.foo = "foo";
 }
}
class ConcreteClass extends AbstractClass {
 constructor() {
   super();
   this.bar = "bar";
 }
 baz() {
   return "baz";
 }
}

let instance = new ConcreteClass();
console.log( instance.bar ); // bar
console.log( instance.foo ); // foo
console.log( instance.baz() ); // baz
console.log( instance instanceof ConcreteClass ); // true
console.log( instance instanceof AbstractClass ); // true

语法看起来是基于类的,但事实上,这是对现有原型的语法甜点。您可以使用ConcreteClass类型进行检查,它会给您函数,因为ConcreteClass是一个规范的构造函数。因此,我们不需要任何技巧来扩展supertypes,也不需要从子类型引用supertype构造函数,而且我们有一个干净的可读结构。但是,我们不能像现在使用方法那样分配属性。这仍在 ES7(的讨论中 https://esdiscuss.org/topic/es7-property-initializers 。除此之外,我们还可以直接在类的主体中声明类的静态方法:

class Bar {
 static foo() {
   return "static method";
 }
 baz() {
   return "prototype method";
 }
}
let instance = new Bar();
console.log( instance.baz() ); // prototype method
console.log( Bar.foo()) ); // static method

实际上,在 JavaScript 社区中有许多人认为新的 java T1 语法是偏离原型 OOP 方法的。另一方面,ES6 类与大多数现有代码向后兼容。该语言现在支持子类,继承不需要额外的库。我个人最喜欢的是,这种语法允许我们使代码更干净,更易于维护。

如何–JavaScript 中的神奇方法

在 PHP 世界中,有重载方法等,也被称为神奇方法(http://www.php.net/manual/en/language.oop5.overloading.php )。这些方法允许我们设置一个逻辑,该逻辑在访问或修改方法的不存在属性时触发。在 JavaScript 中,我们控制对属性(值成员)的访问。假设我们有一个自定义集合对象。为了在 API 中保持一致,我们需要包含集合大小的length属性。因此,我们声明了一个getter(get length),它在访问属性时执行所需的计算。在尝试修改属性值时,setter 将引发异常:

"use strict";
var bar = {
 /** @type {[Number]} */
 arr: [ 1, 2 ],
 /**
  * Getter
  * @returns {Number}
  */
 get length () {
   return this.arr.length;
 },
 /**
  * Setter
  * @param {*} val
  */
 set length ( val ) {
   throw new SyntaxError( "Cannot assign to read only property 'length'" );
 }
};
console.log ( bar.length ); // 2
bar.arr.push( 3 );
console.log ( bar.length ); // 3
bar.length = 10; // SyntaxError: Cannot assign to read only property 'length'

如果我们想在现有对象上声明 getter/setter,我们可以使用以下方法:

Object.defineProperty:
"use strict";
var bar = {
 /** @type {[Number]} */
 arr: [ 1, 2 ]
};

Object.defineProperty( bar, "length", {
 /**
  * Getter
  * @returns {Number}
  */
 get: function() {
   return this.arr.length;
 },
 /**
  * Setter
  */
 set: function() {
   throw new SyntaxError( "Cannot assign to read only property 'length'" );
 }
});

console.log ( bar.length ); // 2
bar.arr.push( 3 );
console.log ( bar.length ); // 3
bar.length = 10; // SyntaxError: Cannot assign to read only property 'length'

Object.defineProperty以及Object.create的第二个参数指定了属性配置(是否可枚举、可配置、不可变,以及如何访问或修改)。因此,我们可以通过将属性配置为只读来实现类似的效果:

"use strict";
var bar = {};

Object.defineProperty( bar, "length", {
 /**
  * Data descriptor
  * @type {*}
  */
 value: 0,
 /**
  * Data descriptor
  * @type {Boolean}
  */
 writable: false
});

bar.length = 10; // TypeError: "length" is read-only

顺便说一句,如果您想要删除对象中的属性访问器,您只需删除该属性即可:

delete bar.length;

ES6 类中的访问器

通过我们可以声明访问器的另一种方式是使用 ES6 类:

"use strict";
/** @class */
class Bar {
 /** @constructs Bar */
 constructor() {
   /** @type {[Number]} */
   this.arr = [ 1, 2 ];
 }
 /**
  * Getter
  * @returns {Number}
  */
 get length() {
   return this.arr.length;
 }
 /**
  * Setter
  * @param {Number} val
  */
 set length( val ) {
    throw new SyntaxError( "Cannot assign to read only property 'length'" );
 }
}

let bar = new Bar();
console.log ( bar.length ); // 2
bar.arr.push( 3 );
console.log ( bar.length ); // 3
bar.length = 10; // SyntaxError: Cannot assign to read only property 'length'

除了公共属性外,我们还可以控制对静态属性的访问:

"use strict";

class Bar {
   /**
    * @static
    * @returns {String}
    */
   static get baz() {
       return "baz";
   }
}

console.log( Bar.baz ); // baz

控制对任意属性的访问

所有这些示例都显示了对已知属性的访问控制。然而,可能有一种情况,我想要一个具有类似于localStorage的可变接口的自定义存储器。这必须是一个具有检索存储值的getItem方法和设置存储值的setItem方法的存储器。此外,其工作方式必须与直接访问或设置伪属性(val = storage.aKeystorage.aKey = "value"时相同。这些可以通过使用 ES6 代理来实现:

"use strict";
/**
* Custom storage
*/
var myStorage = {
     /** @type {Object} key-value object */
     data: {},
     /**
      * Getter
      * @param {String} key
      * @returns {*}
      */
     getItem: function( key ){
       return this.data[ key ];
     },
     /**
      * Setter
      * @param {String} key
      * @param {*} val
      */
     setItem: function( key, val ){
       this.data[ key ] = val;
     }
   },
   /**
    * Storage proxy
    * @type {Proxy}
    */
   storage = new Proxy( myStorage, {
     /**
      * Proxy getter
      * @param {myStorage} storage
      * @param {String} key
      * @returns {*}
      */
     get: function ( storage, key ) {
       return storage.getItem( key );
     },
     /**
      * Proxy setter
      * @param {myStorage} storage
      * @param {String} key
      * @param {*} val
      * @returns {void}
      */
     set: function ( storage, key, val ) {
       return storage.setItem( key, val );
   }});

storage.bar = "bar";
console.log( myStorage.getItem( "bar" ) ); // bar
myStorage.setItem( "bar", "baz" );
console.log( storage.bar ); // baz

总结

本章介绍了如何使用 JavaScript 核心功能以获得最大效果的实践和技巧。在下一章中,我们将讨论模块概念,并对作用域和闭包进行演练。下一章将解释范围上下文和操作它的方法。