NiYingfeng 的博客

记录技术、生活与思考

0%

原文地址:http://www.2ality.com/2014/09/es6-modules-final.html
原文作者:Dr. Axel Rauschmayer

译者:倪颖峰
地址:freefe.cc

在2014年7月底,TC39的另外一个会议,敲定了关于 ECMAScript 6 模块化语法的最后一些细节。本文完整的概述了关于 ES6 模块系统。

1. 当前 JavaScript 的模块系统。

JavaScript 目前没有内置方法来支持模块化,不过社区创造了非常不错的变通方案。两个重要的(可惜相互不兼容)标准是:

  1. CommonJS 模块化:该标准主要被使用在 Node.js 中(Node.js 模块化中有一些超出 CommonJS 的语法的特性)。特点:语法简洁,为同步加载设计,主要使用在服务器端。
  2. 异步模块定义(AMD):该标准最流行的应用就是 RequireJS。 特点:略微复杂的语法使得 AMD 可以不使用 eval() 实现(或者说编译过程),为异步加载设计,主要在浏览器端使用。

以上只是对目前的状况简单粗暴的解释了一下,如果你想更深入的了解一下可以读一下 Addy Osmani 的“使用AMD,CommonJS 和 ES Harmony 写模块化JavaScript”。

2. ECMAScript 6 模块化

ECMAScript 6 模块化的目标是创建一种 CommonJS 和 AMD 使用者都乐意接受的方式:

  1. 类似 CommonJS,拥有简洁的语法,倾向于单一的接口并且支持循环依赖。
  2. 类似 AMD,直接支持异步加载和配置模块加载。

内置语言允许 ES6 模块化超出 CommonJS 和 AMD 规范(之后会详细介绍):

  1. 语法将比 CommonJS 的更简洁。
  2. 结构可以做静态分析(静态检测,优化等)。
  3. 比 CommonJS 做的更好的循环依赖。

ES6 模块化标准包括两部分:

  1. 声明语法(引入与导出)。
  2. 编程式加载接口:用来配置如何加载模块和按条件加载模块。

3. ES6 模块语法概述

有两种导出方式:命名式导出(每个模块可以多个)和定义式导出(每个模块仅一个)。

3.1. 命名式导出(每个模块可以多个)

一个模块可以通过前缀关键词 export 声明来导出多个。

这些导出以名字进行区分,称之为命名式导出。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ----- lib.js ------
export const sqrt = Math.sqrt;
export function square( x ){
return x*x;
}
export function diag( x, y ){
return sqrt( square( x ) + square( y ) );
}

// -------- main.js -------
import{ square, diag } from 'lib';
console.log( square(11) ); // 121
console.log( diag( 4, 3 ) ); // 5

也有其他方式带定义命名式导出(稍后介绍),但是我发现此方式是最方便的:如果没有外层环境,你可以很简单的书写代码,然后以你想要的关键词来标识所有的东西。

如果需要,你也可以导入整一个模块,通过属性命名来指向命名式的导出:

1
2
3
import * as lib grom 'lib';
console.log( lib.square( 11 ) ); // 121
console.log( lib.diag( 4, 3 ) ); // 5

在 CommonJS 语法下的相同代码:有段时间我在 Node.js 下尝试了几种不错的策略来减少我的模块导出时代码冗余。现在我比较喜欢下面这种简单但是略微冗长的风格,有点类似于模块模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ---- lib.js -----
var sqrt = Math.sqrt;
function square( x ){
return x*x;
}
function diag( x, y ){
return sqrt( square( x ) + square( y ) );
}

module.exports = {
sqrt : sqrt,
sqare : sqare,
diag : diag
}

// ------ main.js ------
var square = require( 'lib' ).square;
var diag = require( 'lib' ).diag;
console.log( square( 11 ) ); // 121
console.log( diag( 4, 3 ) ); // 5

3.2. 定义式导出(每个模块仅一个)

在 Node.js 社区很受欢迎的模块导出单一值。当然在需要经常有构造函数和类来创建模型的前端开发中也类一样,一模块一模型。一个 ECMAScript 6 模块可以采用定义式导出,导出最主要的值。定义式导出容易引用。

以下为单函数的 ECMAScript 6 模块:

1
2
3
4
5
6
// ----- myFunc.js -------
export default function(){ };

// ------ main.js --------
import myFunc from 'myFunc';
myFunc();

定义式导出一个类的 ECMAScript 6 模块如下:

1
2
3
4
5
6
// ----- MyClass.js -------
export default class(){ };

// ------ main.js --------
import MyClass from 'MyClass';
let inst = new MyClass();

注:定义式导出声明的操作数是一个表达式,往往不需要名字。代替之的是通过模块的名称来确认。

3.3. 同一模块中使用命名式导出和定义式导出

一下模式在 JavaScript 中非常常见:一个库仅仅是单一的函数,不过通过该函数的属性来提供其他的支持。包括 jQuery 和Underscore.js。下面是简单的 Underscore 作为 CommonJS 模块的写法:

1
2
3
4
5
6
7
8
9
// ----------underscore.js ----------
var _ = function( obj ){};
var each = _.each = _forEach = function( obj, iterator, context ){}

module.esports = _;

// ---------- main.js ---------
var _ = require('underscore');
var each = _.each;

在ES6下,函数 _ 是定义式导出而 each 和 forEach 是命名式导出。 这证明事实上是可以同时进行命名式导出和定义式导出的。例如之前的 CommonJS 模块,以 ES6 来重写模块,如下:

1
2
3
4
5
6
7
// --------underscore.js--------------
export default function( obj ){ }
export function each( obj, itertor, context ){ }
export { each as forEach }

// --------main.js ------------
import _, { each } from 'underscore';

需要注意的是 CommonJS 版本和 ECMAScript 6 版本仅仅是大致相同。后者为扁平式结构,而前者为嵌套式结构。喜欢哪一种风格有自己决定,不过扁平式结构具有做静态分析的优势(下面会提到优点)。CommonJS 风格看起来为了满足对象的需要部分被作为命名空间,可以通过 ES6 模块来实现这种需求并且导出实现。

定义式导出仅仅是另一种形式的命名式导出。

定义式导出事实上是命名式导出使用了特殊名称 default 。即,下面两个是等价的:

1
2
import { default as foo } from 'lib';
import foo from 'lib';

类似的,下面两者也是等价的定义式导出:

1
2
3
4
5
// --------module1.js-----------
export default 123;
// --------module2.js-----------
const D = 123;
export { D as default };

那为何我们还需要命名式到导出呢?

你可能会疑虑,为何我们还需要命名式导出,而不是类似于 CommonJS 形式的单一定义式导出对象?答案就是,你不能通过对象来强制执行一个静态结构并且会丢失相关的优势(下一节描述)。

一个感觉很平凡的,常常在面试中出现的题目,拥有各种实现形式也就显示出 JS 水平的不同。

首先,简单的来一个基本思路的计算方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function maxNumLetter( str ){
var lettersObj = {},
len = str.length,
letter, letterNum, maxLetter, maxNumber = 0;


while( len-- ){
letter = str.substr(len, 1);
letterNum = lettersObj[ letter ] = ( lettersObj[ letter ] || 0 ) + 1;

if( letterNum > maxNumber ){
maxLetter = letter;
maxNumber = letterNum;
}else if( letterNum === maxNumber ){
( maxLetter instanceof Array) ? maxLetter.push( letter ) : ( maxLetter = [ maxLetter, letter ] );
}
}
return maxLetter.toString();
}

上面是基本实现形式,对字符串的每个字母进行遍历,并且在 lettersObj 进行缓存记录,不过每次循环都对字符串进行截取字母看着总是有点不爽,那么可以先将字符串通过 split 进行数组化在进行循环遍历,或者使用字符串的 replace 方式进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function maxNumLetter( str ){
var lettersObj = {},
letterNum, maxLetter, maxNumber = 0;


str.replace(/[a-zA-Z]/g, function( l ){
var lNum = (lettersObj[ l ] || 0 ) + 1;
letterNum = lettersObj[ l ] = ( lettersObj[ l ] || 0 ) + 1;

if( letterNum > maxNumber ){
maxLetter = l;
maxNumber = letterNum;
}else if( letterNum === maxNumber ){
( maxLetter instanceof Array) ? maxLetter.push( l ) : ( maxLetter = [ maxLetter, l ] );
}
});
return maxLetter.toString();
}

使用 replace 添加函数参数形式的特性来替代人工的字母遍历循环。当然由于考虑有相同最多次数字母情况,所以显得比较繁琐。如果还有较为巧妙的方法,欢迎学习交流~

iframe 自适应高度,目前最广泛的实现方式是以 JS 获取 iframe 所载入页面的 body 高度。(在本地 html 文件测试的时候 chrome 会有跨域错误,可以使用 IE 来测试)。其实质就是通过 offsetHeight 或者 scrollHeight 获取到 iframe 内部 body 的高度,再调整 iframe 高度即可。

以下是Test.html 源码初略形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE HTML>
<html>
<head>
<title>iframe 自适应高度</title>
<meta charset="UTF-8"/>
</head>
<body>
<iframe id="auto" name="auto" src="./Test2.html" width="100%" onload="autoHeight()"></iframe>
<script type="text/javascript" charset="utf-8">
function autoHeight(){
var ifr = document.getElementById('auto');
ifr.height = ( ifr.contentDocument && ifr.contentDocument.body.offsetHeight ) ||
( ifr.contentWindow.document.body.scrollHeight );
}
</script>
</body>
</html>

Test2.html 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE HTML>
<html>
<head>
<title>iframe 自适应高度</title>
<meta charset="UTF-8"/>
<style type="text/css">
html,body{
margin: 0;
padding: 0;
}
#block{
background-color: red;
height: 500px;
width: 800px;
}
</style>
</head>
<body>
<div id="block"> </div>
</body>
</html>

当然以上只是简单的实现,具体更严谨的计算高度做好使用一些兼容性不错库作为基础。

原文地址:http://www.2ality.com/2014/08/es6-today.html 原文作者:Dr. Axel Rauschmayer ( 译者: 可以在此处了解 ES5.1 的情况 ) ECMAScript 6 (ES6)听起来依旧感觉离我们很远。毕竟,它要到2015年中旬才能成为标准。但是,它的一些特性逐渐出现在一些浏览器中,有其内置的编译器将 ES6编码 转化为 ES5编码。由于 ES6 的特性集为冻结状态,所以通过后者实现是一个不错的解决方案。 本文简述一下 ECMAScript 6 的特性以及介绍一下现在来使用它们的一些工具。

1. ECMAScript 6 的亮点

本段展现一些 ECMAScript 6 的亮点。

1.1. 新语法

对象字面量 - 属性值简写(用于后面重构的例子):

1
2
3
4
5
6
7
```
let first = 'Jane';
let last = 'Doe'

let obj = { first, last };
// 等同于
let obj = { first:first, last:last }
1
2
3

`对象字面量 - 方法定义:`

1
2
3
4
5
let obj = {
myMethod( arg0, arg1 ){
// ...
}
}
1
2
3

`箭头函数:`

1
2
let arr = [ 1,2,3 ];
let squares = arr.map( x => x\*x );
1
2
3

`扩展操作符:`

1
2
3
4
5
let arr = [ -1, 7, 2 ];
let highest = Math.max( ...arr ); // 7
new Date( ...[ 2011, 11, 24 ] );
// 非破坏性的链接单个元素
let arr2 = [ ...arr, 9, -6 ]; // [ -1, 7, 2, 9, -6 ]
1
2
3

`重构:`

1
2
let [ all, year, month, day ] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec('20110-12-12');
let { first, last } = { first: 'Jane', last: 'Doe' }
1
2
3

`参数默认值`

1
2
3
function findClosestShape( x=0, y=0 ){
// ...
}
1
2
3

`余参`

1
2
3
4
function format( pattern, ...params ){
// return params;
}
console.log( formart( 'a', 'b', 'c' ) );
1
2
3

`通过重构命名参数:`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Entries{
// ...
selectEntries( { from=0, to=this.length } = {} ){
// Long: { from: from=0, to: to=this.length }
// Use 'from' and 'to'
}
}

let entries = new Entriea();
entries.selectEntries( { from:5, to: 15 } );
entries.selectEntries( { from:5 } );
entries.selectEntries( { to: 15 } );
entries.selectEntries( { } );
entries.selectEntries();
1
2
3

`模板字符串:`

1
2
3
4
5
6
7
8
9
10
11
12
13
let str = String.raw`This is a text 
with multiple lines.
Escapes are not interpreted,
\n is not a newline.`

var parts = '/2012/10/Page.html'.match(XRegExp.rx`
^ # match at start of string only
/ (?<year> [^/]+ ) # capture top dir name as year
/ (?<month> [^/]+ ) # capture subdir name as month
/ (?<title> [^/]+ ) # capture base name as title
\.html? $ # .htm or .html file ext at end of path
`);
console.log( parts.year ); // 2012
1
2
3

`类:`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
constructor( nane ){
this.name = name;
}
describe(){
return "Person called" + this.name;
}
}
// 子类
class Employee extends Person{
constructor ( name, title ){
// super.constructor( name )
super( name );
this.title = title;
}
describe(){
// super.describe()
return super() + "(" + this.title + ")";
}
}
1
2
3

`内置函数的子类,如 Error 和 Array:`

1
2
3
class MyError extends Error{
// ...
}
1
2
3

`for-of 循环(对于所有遵守 ES6 迭代规则的对象均有效)`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [ 'foo', 'bar', 'baz' ];

for( let element of arr ){
console.log( element );
}
// foo
// bar
// baz

for( let [ index, element ] of arr.entries() ){
console.log( index + "." + element );
}
// foo
// bar
// baz
1
2
3

`模块化`

1
2
3
4
5
6
7
8
9
10
11
12
13
// lib.js
export const sqet = Math.sqrt;
export function square( x ){
return x * x ;
}
export function diag( x, y ){
return sqrt( square(x) + square( y ) );
}

// main.js
import { square, diag } from 'lib';
console.log( square( 11 ) ); // 121
console.log( diag( 4, 3 ) ); // 5
1
2
3
4
5

### 1.2. 标准库中的新功能

`Object.assign() :`

1
2
3
4
5
6
class Point {
constructor( x, y ){
Object.assign( this, { x, y } );
// ES6中 { x, y } 为 { x : x, y : y } 缩写。
}
}
1
2
3

`Array.prototype.findIndex():`

1
2
[ 6, 8, -5 ].findIndex( x => x<0 ); // 2
[ 6, 8, -5 ].findIndex( x => x<0 ); // -1
1
2
3

`Array.prototype.fill():`

1
2
[ 'a', 'b', 'c' ].fill( 7 ); // [7, 7, 7]
new Array( 3 ).fill( 7 ); // [7, 7, 7]
1
2
3

`新字符串方法:`

1
2
'hello world'.startsWith( 'hello' ); // true
'*'.repeart( 5 ); // *****
1
2
3

`Map(值可以为任何数据格式):`

1
2
3
4
5
6
7
8
let obj = {};
let map = new Map();

map.set( obj, 123 );
map.get( obj ); // 123
map.has( obj ); // true
map.delete( obj ); // true
map.has( obj ); // false
1
2
3

`Set:`

1
2
let arr = [5, 1, 7, 7, 5];
let unique = [ ...new Set( arr ) ]; // [ 5, 1, 7 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

### 1.3. 更多 ECMAScript 6 概要

ECMAScript 6:JavaScript 的下一步棋 es6 特性 https://github.com/lukehoban/es6features#readme es6 利刃 https://github.com/hemanth/paws-on-es6
### 2. 现在就是用 ECMAScript 6
ECMAScript 6 特性开始不断在各种引擎中出现。你可以预览一下 Kangax 的” ECMAScript 6 兼容性表“来知道各个引擎的支持情况。 但对于现实中的项目,你还需要借助一些工具来使当前引擎使用 ECMAScript 6: "es6-tools"(https://github.com/addyosmani/es6-tools) :就是这样一个完整的工具列表。 "ES.next showcase" (https://github.com/sindresorhus/esnext-showcase) :展示了实际使用 ECMAScript 6 的特性。两个实例: 下一版本的 Ember.js 将通过 ES6 的模块 Transplier 来支持 ES6 模块。 下一个版本的 ArgularJS 将通过 Traceur 来支持 ES6 模块。 以下将介绍一些 ES6 应用的工具。
### 3. ECMAScript 6 编译器
如果一个工具需要将 ECMAScript 6 代码转换到 ECMAScript 5,那么它的功能需要超越转换器,所以称之为编译器。两个重要的编译器是 TypeScript 和 Traceur。

### 3.1. TypeScript

跟踪 ECMAScript 6 代码时 TypeScript 开发者所宣称的目标。因此,该语言允许你为 ECMAScript 6 添加注释(为可选)。 TypeScript 可以通过 npm 方便的安装,IDEs Visual Studio 和 WebStorm 均被支持。 当前的 TypeScript 的模块语法有一点滞后于 ECMAScript 6 的规范。它支持两种模块标准:CJS(Node.js) 和 AMD(RequireJS)。

### 3.2. Traceur

Traceur 是最流行的纯 ECMAScript 6 编译器。它对于新特性的支持令人震撼。Traceur 的开发者将它读为 ' tray-SOOR '。有两种方式来使用 Traceur。 静态形式:构建工具(如 Grunt,Gulp,Broccoli 等)的 Traceur 插件可以在开发时将 ES6 文件自动编译为 ES5 文件。查看 es6 工具详情(https://github.com/addyosmani/es6-tools) 动态形式:如果你的 web 应用里有 Traceur 那么你可以通过给 script 标签赋值 type 属性为 module 进行 ES6 编译。

1
2
3
4
5
6
7
8
<div id="output"></div> 
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
<script type="module">
var output = document.getElementById('output');
var w = 'world';
output.textContent = `Hello ${w}!`;
</script>

```

你可以通过传递一个编译参数来使 Traceur 使用确定的模块标准: 1. ES6 的模块加载API 2. AMD(RequireJS) 3. CJS(Node.js)

4. 模块系统 以及 ECMAScript 6

一些现有的和新的 JavaScript 模块系统都通过现成的或者插件的方式来支持 ECMAScript 6。 1. AMD 和 CJS:ES 6 模块编译器(https://github.com/square/es6-module-transpiler)加入了将 ECMAScript 6 模块语法转移为 ECMAScript 5 将其编译为 AMD 或者 CJS。极简风格的解决方案较为吸引人。 2. Browserify:通过基于 Traceur 的 es6ify(https://github.com/thlorenz/es6ify) 转换来支持 ES6。 3. webpack(https://github.com/webpack/webpack)来自现有的 ECMAScript 6 支持。 4. ES6 模块加载补充:基于 ES6 API 和 在Node.js和当前浏览器中动态加载 ES6 模块。以下面两个工具进行补充: SystemJS:基于 ES6 的模块加载器,除了加载 ES6 模块之外还加载 AMD 和 CJS 模块。 jspm.io:为 SystemJS 而出现的包管理器。

5. ECMAScript 6 命令行

JavaScript 命令行是测试特性常用的交互方式。这边简单介绍下可以是使用 ECMAScript 6 语法的命令行。

5.1. ES6 Fiddle

Jeff 编写的 ES6 Fiddle(http://www.es6fiddle.com/)是一款基于 Traceur 的 ECMAScript 6 命令行工具。你可以点击工具栏的一个图标来通过 URL 和唯一的 ID 来保存一个实例。

5.2. Traceur 转码 demo

Traceur 有它自己的 demo 交互页面(http://google.github.io/traceur-compiler/demo/repl.html)。这是一些页面的提示: 1. 你可以打开控制台,通过 console.log() 看到所有你输入到控制台的 ES6 源码。 2. ES6 的源码都被添加到页面的 URL 中,所以你可以通过分享 URL 来分享 ES6 实例。 3. Traceur 在 ES5 中实现了许多 ES6 的方法,意味着你可以在控制台中直接使用例如 Array.form() 之类的方法。 4. let 和 const 变量声明仍在实验阶段,可以通过参数来打开。但是生成的代码较多。幸好这些计划(https://github.com/google/traceur-compiler/issues/6)极大的提高了对 let 和 const 的支持:比如,Traceur 可以用多种方式将 let 转到 var ,这样就不会有较大耗损性的使用let(前瞻性的)。

6. ECMAScript 6 shims

Shims 是一些将未来系统属性模拟到当前系统中的一些库。ECMAScript 6 标准库中包含有趣的新功能,这些功能一般都可以通过库来反向编译为 ECMAScript 5。 1. es6-shim(https://github.com/paulmillr/es6-shim):支持多种 ECMAScript 6 标准库中的特性。 2. ECMAScript 6 promises 的 Shims(Traceur 有它自己的 promise polyfill): RSVP.js(https://github.com/tildeio/rsvp.js/):是 ES6 API 的一个超集。 es6-promise(https://github.com/jakearchibald/es6-promise)是 RSVP.js 的子集,仅包含ES6 API。 Q.Promise 与 ES 6 兼容。

7. 那么继续学习 ECMAScript 5 还有意义么?

众所周知,你现在已经完全可以使用 ECMAScript 6 的代码来避免旧版本的 JavaScript。那是否就意味着我们再也不需要学习 ECMAScript 5了?当然不是,举几个原因: 1. ECMAScript 6 是 ECMAScript 5 的一个超集,新版本 JavaScript 绝对不能破坏旧版本代码。那么你学习 ECMAScript 5并不会白费。 2. 有一部分 ECMAScript 6 特性是替换 ECMAScript 5 的特性的,但是前提是依旧可以使用。两个例子: classes 在内部转为构造函数,method 仍然为函数(因为一直都是)。 3. 只要 ECMAScript 6 被转义为 ECMAScript 5,那么了解编译过程的输出结果还是有用的。你必须要在一段时间内编译为 ES5(或许是几年),直到你可以在所有相关的浏览器中可以依赖 ES6,如现在依靠 ES5 一样。 4. 理解传统代码依旧是很重要的。

原文地址:http://www.2ality.com/2014/07/method-calls.html

原文作者:Dr. Axel Rauschmayer

在 JavaScript 中有两种方式调用方法:通过调度调用(如:obj.someMethod( arg0, arg1 ))和直接调用(如:someFunc.call( thisValue, arg0, arg1 ))。本文解释一下两者的运行方式,以及为何在 ECMScript 6 我们较少的使用直接调用方法形式。

1. 调度式方法调用 vs 直接式方法调用

1.1. 基础:原型链

记住任何在 JavaScript 中对象事实上都是一个或者多个对象组成的链。第一个对象继承后者对象的属性。例如,一个数组 [‘a’, ‘b’]的原型链看起来应该为如下形式:

  1. 实例,保存着元素 ‘a’ 和 ‘b’
  2. Array.prototype,由 Array 构造函数提供的属性
  3. Object.prototype,由 Object 构造函数提供的属性
  4. null,(链的末端,并不是真正意义上链的成员)

你可以通过 Object.getPrototypeOf() 来检测原型链:

1
2
3
4
5
6
var arr = [ 'a', 'b' ];
var p = Object.getPrototypeOf;

p( arr ) === Array.prototype; // true
p( p( arr )) === Object.prototype; // true
p( p( p( arr )));

处在更靠前对象中的属性会覆盖靠后对象中的属性。例如,Array.prototype 中提供的一个数组形式版本的 toString() 方法覆盖了 Object.prototype.toString()

1
2
3
var arr = [ 'a', 'b' ];
Object.getOwnPrototypeNames( Array.prototype ); // ['toString', 'join', ...]
arr.toString();

1.2. 调度式方法调用

如果仔细观察 arr.toString() 的方法调用,可以发现它实际上执行了两步:

  1. 调度:在 arr 的原型链上,取出第一个属性名为 toString 的属性值。
  2. 调用:调用所取到的值,设置隐式参数 this 为接受者 arr 作为其方法调用。

你可以通过函数的 call() 方法来展示这两步:

1
2
var func = arr.toString; // 调度
func.call( arr ); // 直接调用,提供一个明确的 this 值

1.3. 直接式方法调用

在 JavaScript 中有两种形式的直接式方法调用:

  1. Function.prototype.call( thisValue, arg0?, arg1?, … )
  2. Function.prototype.apply( thisValue, argArray? )

call 和 apply 两种方法均在函数上进行调用。这两种方式和一般的函数调用不一样,需要指定 this 的值。call 通过单个参数形式给调用函数提供参数,apply 通过数组的方式提供。

通过动态调度式调用方法有一个问题就是该方法必须在一个对象的原型链上。call() 可以让你指定接受者来直接调用一个方法。这意味着你可以从一个对象中借来一个方法而并不需要存在当前原型链上的。举个栗子:你可以借用Object.prototype.toString 从而使 arr 调用原生的,未被重写的 toString()。

1
Object.prototype.toString.call( arr );

方法在一个不同的对象(不只是继承与它们的构造函数)下调用称为通用。说起 JavaScript 中有一系列方法均是通用的。这列表包括许多数组的方法和所有的 Object.prototype 上的方法(它与所有的对象一起工作,从而使隐式的通用)。

2. 使用直接式方法调用的实例

2.1. 通过数组形式给方法传参

一些方法接受多个参数,但是每个参数只有一个值。那么你时候希望以数组形式进行传参呢?

比如,push() 允许你具有破坏性的将一些值添加到数组中:

1
2
3
var arr = ['a', 'b'];
arr.push( 'c', 'd' ); // 4
arr; // ['a', 'b', 'c', 'd']

但是你不能破坏性的添加一整个数组。你可以通过 apply() 来解决这个限制问题:

1
2
3
var arr = ['a', 'b'];
Array.prototype.push.apply( arr, [ 'c', 'd' ] ); // 4
arr; // ['a', 'b', 'c', 'd']

类似的,Math.max() 和 Math.min() 也只能使用单个值:

1
Math.max( -1, 7 ,2 ); // 7

通过apply(),你可以对他们使用数组:

1
Math.max.apply( null, [ -1, 7, 2 ] ); // 7

2.2. 一个类数组对象转换成一个数组

在 JavaScript 中一些对象为类数组,它们大致上是数组,但是没有任何数组方法。看下两个例子。

首先,函数的特殊变量 arguments 是类数组, 它有一个 length 和 可以下标访问元素。

1
2
3
var args = function(){ return arguments; }( 'a', 'b' );
args.length; // 2
args[0]; // 'a'

但是 arguments 并不是 Array 的一个实例,并且没有 forEach() 方法。

1
2
args instanceof Array; // false
args.forEach; // undefined

其次,DOM 方法 document.querySelectorAll() 返回的 NodeList 实例。

1
2
document.querySelectorAll('a[href]') instanceof NodeList; // true
document.querySelectorAll('a[href]').forEach; // undefined

再次,一些复杂操作,你需要现将类数组转化成数组形式。这可以通过 Array.prototype.slice() 来实现。该方法将接受者中的元素拷贝出来放置到新的素组中:

1
2
3
var arr = ['a', 'b'];
arr.slice(); // ['a', 'b']
arr.slice() === arr; // fasle

如果直接式调用 slice(),可以将一个 NodeList 转化为一个数组 :

1
2
3
4
5
var domList = document.querySelectorAll( 'a[href]' );
var links = Array.prototype.slice.call( domList );
links.forEach(function( link ){
console.log( link );
});

也可以将 arguments 转化为数组:

1
2
3
4
5
function format(pattern) {
var params = Array.prototype.slice.call(arguments, 1);
return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']

2.3. 正确是使用 hasOwnPrototype()

obj.hasOwnPrototype(‘prop’) 可以检测对象 obj 是否有自己(非继承的)的 prop 属性。

1
2
3
4
var obj = { prop: 123 };
obj.hasOwnProperty('prop'); // true
'toString' in obj; // true
obj.hasOwnProperty('toString'); // fasle

然而,如果 Object.prototype.hasOwnPrototype 被覆盖过, 那么通过正确的调度式调用 hasOwnPrototype 可能回发生错误。

1
2
3
var obj = { hasOwnprototype : 123 };
obj.hasOwnPrototype( 'toString' );
// TypeError: number is not a function

如果一个对象的原型链上没有Object.prototype,那么通过调度式调用 hasOwnPrototype 可能出错。

1
2
3
var obj = Object.create( null );
obj.hasOwnPrototype( 'toString' );
// TypeError: undefined is not a function

两者,可以使用直接式调用 hasOwnPrototype 的方法来解决

1
2
3
4
5
var obj1 = { hasOwnProperty: 123 };
Object.prototype.hasOwnProperty.call(obj1, 'hasOwnProperty') ; // true

var obj2 = Object.create(null);
Object.prototype.hasOwnProperty.call(obj2, 'toString') ; // false

2.4. 避免中间对象

对字符串应用一个数组的方法,比如 join() 转化,一般有两步:

1
2
3
4
var str = 'abc';
var arr = str.split(''); // step 1
var joined = arr.join('-'); // step 2
console.log( joined ); // a-b-c

字符串是类数组,可以作为数组通用方法的 this 值:
因此,直接式的调用可以避免第一步:

1
2
var str = 'abc';
var joined = Array.prototype.join.call( str, '-' );

类似的,你可以对字符串分割为数组后,或者通过直接式方法调用来使用 map()。

1
2
3
4
5
6
function toUpper( x ){
return x.tiUpperCase();
}

'abc'.split('').map( toUpper ); // [ 'A', 'B', 'C' ]
Array.prototype.map.call( 'abc', toUpper ); // [ 'A', 'B', 'C' ]

记住,直接式调用可能可以更高效,但是形式是不为优雅。但那是值得的。

3. Object.prototype 和 Array.prototype 简短形式

你可以通过一个空对象字面量(其原型是 Object.prototype)来访问 Object.prototype 的方法。如下面两种直接式方法调用是等价的:

1
2
Object.prototype.hasOwnPrototype.call( obj, 'propKey' );
{}.hasOwnPrototype.call( obj, 'propKey' );

对于 Array.prototype 使用相同方式:

1
2
Array.prototype.slice.call( arguments );
[].slice.call( arguments );

这种形式变得颇为流行。相对于较长版本来说,可能没有更好的显示出作者的意图,但是它更为简短,速度方面也并没有很大的差距(译者注: http://jsperf.com/array-prototype-slice-vs-slice2http://jsperf.com/object-prototype-tostring-call-vs-tostring 显然字面量形式肯定会慢一点,要创建完对象或者数组之后再去原型上查询,但是差距并不是很大)。

4. 在 ECMAScript 6中 直接式调用的替代方案

感谢 ECMAScript 6 的新特性,你可以不再那么频繁的使用直接式方法调用了。

4.1. 扩展操作符 几乎可以替代 apply()

是我们用过 apply() 使用直接式方法调用的唯一原因是我们要转换一个数组为 arguments 显得比较麻烦,这也是 ECMAScript 6 有了扩展操作符(…)的原因。它为调度式调用方法提供了此功能。

1
Math.max( ...[ -1, 7, 2 ] ); // 7

另一个例子:

1
2
3
let arr = ['a', 'b'];
arr.push( ...['c', 'd'] ); // 4
arr; // ['a', 'b', 'c', 'd']

另一个边界==便捷, 扩展在 new 操作符中也是起作用的:

1
new Date( ...[ 2011, 11, 24 ] );

记住,apply() 是无法在 new 操作符中使用,上面的形式需要通过复杂的形式围绕 ECMAScript 5 来实现。

4.2. 在 ECMAScript 6 中类数组对象将不再成为累赘

一方面, ECMAScript 6 拥有 Array.form() 方法,一个转化类数组对象为数组的简单方式。

1
2
3
4
5
let domLinks = document.querySelectorAll( 'a[href]' );
let links = Array.form( domLinks );
links.forEach(function( link ){
console.log( link );
});

另一方面,你不在需要类数组 arguments,因为 ECMAScript 6 可以获取其余参数(三个点声明):

1
2
3
4
function format( pattern, ...params ){
return params;
}
console.log( format('a', 'b', 'c') ); // [ 'b', 'c' ]

4.3. hasOnPrototype()

hasOnPrototype() 基本是通过对象实现映射。幸好, ECMAScript 6 中有有了内置的 Map 数据结构,所以可以使你更少的使用 hasOnPrototype()。

4.4. 避免中间对象

Array.form() 可以式转换和映射一步完成,也可以在第二个参数传入回调函数。

1
2
Array.from('abc', ch => ch.toUpperCase())
// [ 'A', 'B', 'C' ]

也可以作为两步:

1
2
'abc'.split('').map(function (x) { return x.toUpperCase() })
//[ 'A', 'B', 'C' ]

装饰者模式,Decorator Pattern。在不改变原类和继承的情况下动态扩展对象功能,通过包装一个对象来实现一个新的具有原对象相同接口的新的对象。

装饰者模式特点:

  1. 不修改原对象的原本结构来进行功能添加。
  2. 装饰对象和原对象具有相同的接口,可以使客户以与原对象相同的方式使用装饰对象。
  3. 装饰对象中包含原对象的引用,即装饰对象为真正的原对象在此包装的对象。

装饰者模式可以为对象添加功能从而代替了编写大量子类的情况。

以JS设计模式实例为例,首先自行车商店有4种类型自行车:

1
2
3
4
var ABicycle = function(){ ... };
var BBicycle = function(){ ... };
var CBicycle = function(){ ... };
var DBicycle = function(){ ... };

当自行车商店需要为每类自行车推出附加功能或者配件,比如前灯,车篮,改色等的时候,其最基本的方式就是为每一种组合创建一个子类,如有铃铛的A类车,黄色的B类车,有车篮的C类车等等,

1
2
3
4
var ABicycleWithBell = function(){ ... };
var BBicycleWithColorYellow = function(){ ... };
var CBicycleWithColorBule = function(){ ... };
...

的确,就犹如你第一眼看到的感觉一样,这搞法果断不靠谱,n种配置,到最后就会生成4n+4种类(包括原始的4个父类),那是决然不可取的。

当然我们可能的第一想法便是可以将颜色,车篮什么的设为实例属性,在类中由传递的参数进行控制生成,这样的确不错没什么问题,不过对于一些功能上的扩展可能就心有余而力不足了,比如自行车添加变速功能,刹车加强功能,防盗功能等,那么对于添加这些功能使用装饰者模式则更为简便了(如果说将更能内置于类中,再由传参进行处理,在功能较多的情况下显然也比较不靠谱)。

首先需要一个基础自行车A的类

1
2
3
4
5
6
7
8
function ABicycle(){ }
ABicycle.prototype = {
wash : function(){ },
ride : function(){ },
getPrice : function(){
return 999;
}
}

接下来就是给自行车装上铃铛,响铃的功能:

最简单的应该就是直接包装对象实例:

1
2
3
4
5
6
7
8
9
10
11
12
function bicycleBell( bicycle ){
var price= bicycle.getPrice();

bicycle.bell = function(){
console.log("ding! ding! ding!");
};

bicycle.getPrice = function(){
return price + 100;
};
return bicycle;
}

那么使用

1
2
var bicycleA = new ABicycle();
bicycleA = bicycleBell( bicycleA );

如果是下面这种形式

1
2
3
bicycle.getPrice = function(){
return bicycle.getPrice() + 100;
};

这种形式当然就会导致无限循环调用~

的确这种包装方法不是将对象再次包装成构造函数,仅仅只是调整实例对象,既方便又简单,不过对于类似getPrice方法这种返回常量数据结果的倒是问题也不大,可以使用如上面闭包形式的方式来保存。

但如果对于其功能的在包装需要调用,再用闭包形式来先进行原始方法的保存,就会赶脚很繁琐不变。

那么可以来看一下下面这种装饰方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function BicycleBell( bicycle ){
this.bicycle = bicycle;
}

BicycleBell.prototype = {
wash : function(){
return this.bicycle.wash();
},
ride : function(){
return this.bicycle.ride();
},
getPrice : function(){
return this.bicycle.ride() + 100;
},
bell : function(){
console.log("ding! ding! ding!");
}
}

包装实例,再次模拟原始类,将实例作为参数包装,提供与原始类一样的接口。这种方式很好的解决了对于某些需要修改并且依赖原始该方法的方法生成形式。当然其也有自己的缺点,这个装饰者太过于繁琐,所有的,比如加速,切换颜色,车篮什么的装饰者每一个都必须如此形式的话势必代码有点杂乱多。

那么,提取出来吧

首先需要有一个继承的方法,并且有指向父类的prototype

1
2
3
4
5
6
7
8
9
10
11
12
function extend( subClass, superClass ){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;

// 此处指向superClass的prototype 比直接保存superClass使用更为方便
subClass.superclass = superClass.prototype;
if( superClass.prototype.constructor === Object.prototype.constructor ){
superClass.prototype.constructor = superClass;
}
}

再来一个各个装饰者可以依赖继承的中间装饰者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function BicycleDecorator( bicycle ){
this.bicycle = bicycle;
}
BicycleDecorator.prototype = {
wash : function(){
return this.bicycle.wash();
},
ride : function(){
return this.bicycle.ride();
},
getPrice : function(){
return this.bicycle.ride();
}
}

然后么很巧妙的使用extend

1
2
3
4
5
6
7
8
9
10
11
12
13
var BicycleBell = function( bicycle ){
// 继承 BicycleDecorator 内部 this定义的数据或者方法
BicycleBell.superclass.constructor.call( this, bicycle );
}
// 继承 BicycleDecorator.prototype 并且添加 BicycleBell.superclass 指向 BicycleDecorator.prototype
extend( BicycleBell, BicycleDecorator );
// 添加或者修改
BicycleBell.prototype.bell = function(){
console.log("ding! ding! ding!");
}
BicycleBell.prototype.getPrice = function(){
return this.bicycle.getPrice() + 100;
}

一样的方式进行实例包装使用

1
2
3
var bicycleA = new ABicycle();
bicycleA = new BicycleBell( bicycleA );

上述方式就是一种较为不错的装饰者模式形式,更完善一些可能需要对BicycleDecorator,以及BicycleDecorator.prototype对象定义时使用对最初类遍历方式来获取各个原有接口。

当然对于这几种方式可以按需进行使用,有针对性的处理才是最有方案。

原文地址:http://www.2ality.com/2014/05/this.html
原文作者:Dr. Axel Rauschmayer

在JavaScript中,特殊变量 this 相对来说较为复杂,因为它不仅仅只在面向对象设定中出现,其随处可见。这里会解释一下 this 的工作原理以及会引起问题的地方,最佳实践总结。

理解 this,最好的方式是区分被使用的三种类型的位置:

  • 函数内部:this 是一个额外的隐式的参数。
  • 函数外(顶层作用域):this 在浏览器中指向全局对象,Node.js中指向一个模块的暴露接口。
  • 在传递给 eval() 的字符串中:eval() 也许会取得当前 this 的值,或者是将其设置为全局对象,取决于是直接或者间接的调用。

来看一下各个类型。

1. 在函数内部的 this

这是 this 一种最常用的方式,因为在 JavaScript 中,函数以三种不同的角色代表了所有的可调用的结构形式。

  1. 确切的函数(this 在松散模式中为全局对象,在严格模式中为 undefined )
  2. 构造函数(this 指向新创建的实例)
  3. 方法(this 指向方法调用的接受者)

在函数中,this 被常常认为是一个额外的隐式的参数。

1.1 在确切的函数中的 this

在确切的函数中,this 的值依赖于其所在的模式:

松散模式:this 指向全局对象(浏览器下为windows)

1
2
3
4
function sloppyFunc(){
console.log( this === window )
}
sloppyFunc();

严格模式:this 的值为 undefined

1
2
3
4
5
function strictFunc(){
"use strict";
console.log( this === undefined );
}
strictFunc();

就是说,this 是一个隐式的,设置有默认值(window 或者 undefined)的参数。但是,你可以使一个函数通过 call() 或者 apply() 调用来明确的指定 this 的值。

1
2
3
4
5
6
7
8
9
10
11
12
function func( arg1, arg2 ){
console.log( this );
console.log( arg1 );
console.log( arg2 );
}
func.call( { a:'a' } , 'b', 'c' );
// Object {a: "a"}
// b
// c
func.apply( { a:'a' } , [ 'b', 'c' ]);
// Object {a: "a"}
// b

1.2 在构造函数中的 this

你通过 new 操作符来调用的函数即是构造函数,该操作符创建一个新的对象,并把它通过 this 传递给构造函数:

1
2
3
4
5
6
7
var savedThis;
function Constr(){
savedThis = this;
}

var inst = new Constr();
console.log( savedThis === inst );

new 操作符使用 JavaScript 实现大致如下(一个更为准确复杂的实现 http://speakingjs.com/es5/ch17.html#_the_new_operator_implemented_in_javascript):

1
2
3
4
5
function newOperator( Constr, arrayWithArgs ){
var thisValue = Object.creat( Constr.prototype );
Constr.apply( thisValue, arrayWithArgs );
return thisValue;
}

1.3 方法中的 this

在方法中,所有的事情都和传统的面向对象语言类似: this 指向接受者,即方法被调用的那个对象。

1
2
3
4
5
6
var obj = {
method : function(){
console.log( this === obj );
}
}
obj.method();

2. 顶级作用域的 this

在浏览器环境中,顶级作用域是全局作用域, this 指向全局作用域(诸如 window 之类):

1
2
3
<script>
console.log( this === window ); // true
</script>

在 Node.js 中,你基本在模块中编写代码。那么当前顶级作用局就是一个特定的模块作用域。

1
2
3
4
5
6
7
8
// (在 Node.js 的具体模块中)
// 'global'(非 window) 指向全局对象
console.log( Math === global.Math ); // true

// 'this' 并不指向全局对象
console.log( this !== global ); // true
// 'this' 指向模块的暴露接口
console.log( this === module.exports );

3. eval() 中的 this

eval() 既可以被直接调用(通过一个确切的方法调用)也可以被间接调用(通过另一些方式)。以下是具体解释。

如果 eavl() 被间接调用, this 指向全局对象:

1
( 0, eval )( " this === window" );

否者,如果 eval() 被直接调用,那么 this 指向 eval() 所处的执行环境。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function sloppyFunc(){
console.log( eval("this") === window );
}
sloppyFunc(); // true

function strictFunc(){
"use strict";
console.log( eval("this") === undefined );
}
strictFunc(); // true


var savedThis;
function Constr(){
savedThis =eval(" this");
}

var inst = new Constr();
console.log( savedThis === inst ); // true


var obj = {
method : function(){
console.log( eval("this") === obj );
}
}
obj.method();

4. this 的相关陷阱

这里有3个值得注意的和 this 有关的陷阱。 记住,严格模式可以使得每种情况变得正常,因为在确切的函数中 this 都会为 undefined ,当出错的时候会得到警告。

4.1 忘记 new

如果你调用构造函数却忘记了 new 操作符,那么你会意外的调用一个实实在在的函数。故而, this 就不会指向当前值。在松散模式中,this 指向window,并且会创建全局变量。

1
2
3
4
5
6
7
8
9
10
11
function Point( x, y ){
this.x = x;
this.y = y;
}

var p = Point( 7, 5 ); // 忘记 new 操作符
console.log( p === undefined ); // true

//创建全局变量
console.log( x ); // 7
console.log( y );

幸亏有严格模式,你可以获取到警告(this === undefined)

1
2
3
4
5
6
7
function Point( x, y ){
"use strict";
this.x = x;
this.y = y;
}

var p = Point( 7, 5 );

4.2 获取方法不当

如果你获取方法的值(而非调用),你会将方法又转变为函数。结果值的调用为函数的调用而非方法的调用。这种获取方式发生在你将一个方法作为参数传递给一个函数或者方法的时候。实际环境中的例子包括 setTimeout() 和 时间注册处理程序。这边会使用函数 callIt() 来同时模拟用例。

1
2
3
4
/* 与 setTimeout() 和 setImmediate() 类似 */
function callIt( func ){
func();
}

如果你调用一个松散模式下的函数, this 指向全局对象,并且创建全局变量:

1
2
3
4
5
6
7
8
9
10
var counter = {
count : 0,
inc : function(){
this.count++;
}
}
callIt( counter.inc );

console.log( counter.inc ); // 0, 无效
console.log( count );

如果你调用严格模式下的函数,this 为undefined, 代码也将会无效。但是会得到一个警告。

1
2
3
4
5
6
7
8
var counter = {
count : 0,
inc : function(){
"use strict";
this.count++;
}
}
callIt( counter.inc );

可以使用 bind() 开解决:

1
2
3
4
5
6
7
8
9
var counter = {
count : 0,
inc : function(){
"use strict";
this.count++;
}
}
callIt( counter.inc.bind( counter ) );
console.log( counter.count );

bind() 创建一个接受的 this 值为 counter 的新函数。

4.3 this 跟踪

当你使用一个确实的函数来代替方法,很容易忘记前者是有自己的 this值(尽管经常不使用)。因此你无法将前者的 this 指向方法的 this,由于它是跟踪状态的。看一下出错的实例:

1
2
3
4
5
6
7
8
9
10
11
var obj = {
name : 'Jane',
friends : [ 'Tarzan', 'Cheeta'],
loop : function(){
'use strict';
this.friends.forEach(function( friend ){
console.log( this.name + ' knows ' + friend );
});
}
}
obj.loop(); // TypeError: Cannot read property 'name' of undefined

在上面例子中,this.name 获取出错,原因是函数的 this为undefined,他与方法 loop() 内的 this 不一样,来看看3中解决方案。

解决方案1: that = this。将 this 设置给一个变量,使其不再跟踪状态(另一个常用的变量名为 self)再使用它。

1
2
3
4
5
6
7
loop : function(){
'use strict';
var that = this;
this.friends.forEach(function( friend ){
console.log( that.name + ' knows ' + friend );
});
}

解决方案2:bind()。使用 bind() 来创建一个 this 常指向当前值的函数(下面实例方法的 this)。

1
2
3
4
5
6
loop : function(){
'use strict';
this.friends.forEach(function( friend ){
console.log( this.name + ' knows ' + friend );
}.bind( this ));
}

解决方案3:forEach 的第二个参数。该方法有第二个参数用来传递回掉函数在某一目标上进行调用。

1
2
3
4
5
6
loop : function(){
'use strict';
this.friends.forEach(function( friend ){
console.log( this.name + ' knows ' + friend );
}, this );
}

5. 最佳实践

概念上来说,我认为确实的函数并不具有它们自己的 this,认为上述解决方案只是保持这个错觉。ECMAScript 6通过箭头函数来支持 this 的方法,函数没有其自己的 this 值。使用这些函数,你可以无忧无虑的使用 this,因为不会跟踪。

1
2
3
4
5
6
7
8
loop : function(){
'use strict';
// forEach 的参数就是箭头函数
this.friends.forEach( friend =>{
// 'this' 就是 loop 的 'this'
console.log( this.name + ' knows ' + friend );
});
}

我不喜欢 APIs 中像一些普通函数的参数那样使用 this:

1
2
3
4
5
beforeEach(function(){
this.addMatchers({
toBeInRange : function(){ ... }
});
});

箭头函数将一个隐式的参数转变为显式的,使得行为更为明确和兼容。

1
2
3
4
5
beforeEach( api => {
api.addMatchers({
toBeInRange : function(){ ... }
});
});

适配器模式在维基百科上的属于 Wrapper function 包装函式,其目的是用来呼叫另一个函式。在面向对象编程中,又被称为方法委任,method delegation。而适配器模式可以说是包装函式其中一种功能形式的存在。

简单的说,适配模式主要是为了解决一些接口不兼容产生的解决方法。借助于适配器我们可以在不修改这些不兼容接口的情况下给使用者提供统一的包装过的适配接口。表面上又感觉和之前的门面模式比较像,均是对其他对象或者接口进行包装再呈现,而适配器模式偏向的是解决兼容性问题,门面模式则偏向方便性为原则。

比如一个简单的学生查询学科成绩的方法:

1
2
3
4
function selectScore( name, id, course_id ){
// arguments 姓名 学号 课程id
...
}

当我需要一个班级某门学科的整体成绩列表,而我手上只有每个学生如下的数据

1
2
3
4
5
[
{ name: 'lily', studentID: '0911' },
{ name: 'suny', studentID: '0912' },
...
]

我需要查询 英语 其课程ID为 101,那么对于该任务,写一个适配器方式是很恰当不过的

1
2
3
function selectEnglishScore( stutentObj ){
selectScore( stutentObj.name, stutentObj.studentID , 101);
}

这是一个最简单的关于适配器来处理参数方面兼容的形式。

适配器模式可以想象为我们日常生活中经常使用的接口转换器,实现两个或者多个不同的数据存储器进行数据交换,适配各自不同数据输出口的工具,比如以前的一些PS2插口, USB接口,以及当前SD卡,移动硬盘各式各样的接口,接**换器是一件灰常有用的小工具。适配器模式的方法也是如此。

其实简单的来说,适配器模式意义上很简单 - 适配,解决兼容问题。对于应用场景来说,主要是为了解决新旧代码之间不想重构而实行的中庸的方式,或者更换一些类库,升级不兼容版本类库的时候的一种“懒惰”方法。对于适配器模式的确会造成一些不同的看法,比如说原本是需要完全重构的代码却使用了几个简单的适配器模式方法就搪塞过去了。随着这些行为的越来越过,使得代码的质量日渐不足,甚至令后来的程序猿感到愤愤不已。

所以对于适配器模式使用的情况我们需要做好多方面的考虑,其实对于所谓的程序设计模式而言均是如此。原本是为了优化而做的工作,可别到头来适得其反。

拖拖拖拖啦啦啦啦啦~ n久未更新更新JS设计模式了,不能回家墓祭祖先,那就在此墓祭墓祭JS吧,T T(本文实例主要来自 JavaScript设计模式)。

之前在桥接模式中简单的说过(额,桥接模式有点混,下次再写一写),门面模式本质是实现一个简单的统一接口来处理对各个子系统接口的处理和调用。其和桥接模式的不同之处便在于桥接模式中的各个类是完全独立的,桥接模式只在必要的时候将这些类关联起来。

而门面模式则有点不同。门面模式其实可以很形象的比作为一家咖啡店的店面窗口,客户只需要说明自己是需要摩卡还是白咖啡,也就是说咖啡店提供给客户的只是各类咖啡的选择接口,而将内部的各个子类行为封装起来,比如磨咖啡豆,冲泡,加奶等过程并不展现给客户,因为每一种咖啡有其不同的生产形式,这样也使得客户更方便的选择所喜欢的咖啡。

门面模式的优点就在于我们将客户与较为复杂的子系统方法和接口分离开来,降低用户与各个子系统间耦合度来提高代码质量。

下面是一个纯粹形式化的例子:

1
2
3
4
5
6
7
8
9
10
function a(x){
// do something
}
function b(y){
// do something
}
function ab( x, y ){
a(x);
b(y);
}

当然形式上与桥接模式有很大程度上的类似,下面的小例子可以感受下其和桥接模式的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var N = window.N || {};
N.tools = {
stopPropagation : function( e ){
if( e.stopPropagation ){
e.stopPropagation();
}else{
e.cancelBubble = true;
}
},

preventDefault : function( e ){
if( e.preventDefault ){
e.preventDefault();
}else{
e.returnValue = false;
}
},

stopEvent : function( e ){
N.tools.stopPropagation( e );
N.tools.preventDefault( e );
}
}

一个小的阻止事件冒泡以及阻止默认事件工具方法,从其代表性的 stopEvent 不难看出其实质与桥接模式的区别。其上面两个方法 stopPropagation 和 preventDefault 很会令人觉得类似于适配器模式,的确是很类似与适配器模式,不过适配器模式的主要针对于将接口进行适配包装,使其适用于各种不同兼容性的环境。按本人的理解则是,如果提供兼容性的信息使其方法在不同环境下生成不同的方法,比如一些匿名自调用函数根据判断返回不同函数的类似形式,而不是说每次运行再在函数内部进行判断运行,则称其为适配器模式可能为更恰当一些~

对于门面模式的一大好处就是对函数的组合上,犹如上面纯粹模式的例子,门面模式形成的组合函数又称为便利函数(convenience function),便利,便利,便利,好吧,可能对于门面模式的概念又要模糊了,来一例:

我们需要将id为content的div元素设置文本颜色 red,那么简单的代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var element = document.getElementById('content');
content.style.color = 'red';
//如还想设置字体大小为20px
content.style.fontSize = '20px';

那么当有一个元素需要设置多种属性时,

function setStyles( id, styles ){
var element = document.getElementById( id );
for( var key in styles ){
if( styles.hasOwnProperty( key ) ){
element.style[ key ] = styles[ key ];
}
}
}

setStyles( 'content', {
color : 'red',
fontSize : '20px'
} );

setStyles就相当于一个便利函数,也可以视作为门面元素,不过是相对于最简单一类。

如果说是具有好几个元素,均需要设置相同的一批属性的话,那么将 setStyles 包装一下,将其嵌入在另一个门面元素之中,组合成为一个结构相对复杂的门面模式实例:

1
2
3
4
5
function setCSS( ids, styles ){
for( var i = 0,len = ids.length; i<len; i++ ){
setStyles( ids[i], styles );
}
}

从setCSS中可以看出,对于使用setCSS的用户来说,根本不知道其内部的 setStyles 代码形式。可想而知,当一块代码逻辑较为复杂,调用许多各个接口等的时候,我们使用门面模式将其封装,可以带来很大的便利性。

总结

门面模式大致上可以分为两个小类, 某块代码反复出现,比如函数a的调用基本都出现在函数b的调用之前(虽然基本上没有那么简单的形式),那么你可以考虑考虑将这块代码使用门面实例包装包装来优化结构。还有一种即是对于一些浏览器不兼容的API,放置在门面模式内部进行判断,处理这些问题最好的方式便是将跨浏览器差异全部集中放置到一个门面模式实例中来提供一个对外接口。

当然也需要很注意的是,对于门面模式的滥用其产生的结果是超级严重的,不但是代码整体结构感觉较为散架,还是代码可读性严重降低,不寻找一处BUG可能需要从一个门面实例中找到另一个,再联系到第三第四个,会使代码维护的程序崽子非常反感的~

好吧,本以为node中的Buffer只是nodeJS中挺小的一块,仔细的翻阅了一些资料之后才发现,冰川总是将其巨大的屁股藏在海平面以下的,这次也是主要简单的讲一下关于Buffer比较浅的一些东西(针对Node初学者啦~)

Buffer 是什么?

对于JavaScript来说,对Unicode编码的数据很容易处理的,但是对于二进制数据就没什么处理方法了。

但是在一些TCP数据流或者在操作一些文件数据流的时候,字节流的处理方法还是必须的。

nodeJS中便出了处理的策略方法~提供了与String类似对等的全局构造函数Buffer(与其说与String对等,还不如说与Array类似但是有些不同方面还是需要注意),全局那么自然就不要每次的require了。

Buffer则node中储存二进制数据的中介者。

创建Buffer实例的方式:

1
new Buffer( size );

和数组类似,给定所需要创建Buffer对象的大小。

1
new Buffer( array );

给定二进制数据的一个数组形式排列

1
new Buffer( str, [ encoding ] );

给与某一特定数据类型数据,以及其编码类型(默认均为utf8)

以及还有一些操作Buffer实例的方法:

一个Buffer的形式非常类似于一个整数数组,与字符串还是有一定的区别的。

JS程序员都比较熟悉,对于字符串来说可以视为可读的,每次的修改,复制都会得到一个新的字符串,不会影响之前的字符串,就如有一个字符串a,将 var b = a; a 和 b 是各会拥有独立的一份字符串数据,相互独立,与对象类似于指针的形式不同(按值传递和按址传递)。

不过对于字符串的兄弟Buffer来说,作为对象的Buffer含有的Array的血更多一点。

但是对于Buffer的一些方法进行使用的时候,还是需要注意其和数组一些不一致的地方

在将创建的Buffer截取一段之后,对返回的Buffer段进行修改,还是会影响原Buffer,两者仍是关联的。这类似于一个储存一个个对象的数组,Buffer这个类数组中每一个元素非原始值,而是引用值。

对于希望复制一份独立的Buffer拷贝,Buffer提供了专门的方法

1
Buffer.copy(targetBuffer, [targetStart], [sourceStart], [sourceEnd]);

相对来说并没有直接的直接拷贝副本的方法,只能新建一个长度相等的Buffer,然后在原Buffer上调用copy方法,参数中还可以设置copy的启事与结束位置等。

对于更多的信息可以查看node的Buffer文档 http://nodejs.org/api/buffer.html

什么时候才需要使用Buffer

好了,以上就是对Buffer的简单介绍,其实那都是虚的,什么时候才需要使用Buffer呢?

其实之前已经说过在Node中,许多地方的数据流均是使用的Buffer类型,以便于来处理一些二进制数据(比如读取文件数据,http请求中的post传递的数据等)。

1
2
3
4
5
var fs = require('fs');
var rs = fs.createReadStream('chinese.md');
rs.on("data", function (chunk){
console.log( Buffer.isBuffer( chunk ) )
});

从上面可知其均为 Buffer 类型片段,那么在一些情况下也会使得我们遇到一些隐藏的问题:

1
2
3
4
5
6
7
8
9
var fs = require('fs');
var rs = fs.createReadStream('chinese.md' , { highWaterMark: 5 } );
var data = '';
rs.on("data", function (chunk){
data += chunk;
});
rs.on("end", function () {
console.log(data);
});

会出现乱码现象,无法正常显示。

一个中文字占了3个Buffer单位,但是在数据流分块的时候将一个中文的3个标识分开,再做 toString的时候救会发生出现这种乱码的情况。解决方式就是在拿到的分块数据千万不能直接进行类似toString式转义,将每段Buffer保存,最后合并成一个大的Buffer后在进行转义:

1
2
3
4
5
6
7
8
9
10
11
var fs = require('fs');
var rs = fs.createReadStream('chinese.md', { highWaterMark: 5 } );
var dataArr = [], len = 0, data;
rs.on("data", function (chunk){
dataArr.push(chunk);
len += chunk.length;
});
rs.on("end", function () {
data = Buffer.concat( dataArr, len ).toString();
console.log(data);
});

上述实例中我们可能还发现一个问题,将 { highWaterMark: 5 } 参数去掉后其实两个均没有问题,原因是因为默认的stream中切割data块单位是8K,所以可以在一个单位内容下这个小实例,所以没什么问题。一旦说数据很大,那么也会有相应的问题了。(之前一直拿txt测试了好久,发现怎么也不对,发现是编码方式问题)。

顺便说一下在拼接性能方面,其实Buffer与String相比并不是很差:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var buf = new Buffer('this is text concat test'),
str = 'this is text concat test';
console.time('buffer concat test');
var list = [];
var len= 100000 * buf.length;
for(var i=0;i<100000;i++){
list.push(buf);
len += buf.length;
}
var s1 = Buffer.concat(list, len).toString();
console.timeEnd('buffer concat test');
console.time('string concat test');
var list = [];
for (var i = 100000; i >= 0; i--) {
list.push(str);
}
var s2 = list.join('');
console.timeEnd('string concat test');

好吧,我承认写的有点不耐烦了,那么就这样吧,相当于简单的做个标签,有需要再查吧~ 哦吼吼~