es6

第一章. ECMAScript 6簡介
(1)ECMAScript和JavaScript的關係
(2)ECMAScript的歷史
(3)部署進度
(4)Babel轉碼器
(5)Traceur轉碼器
(6)ECMAScript 7
第二章.let和const命令
(1)let命令
(2)塊級做用域
(3)const命令
(4)全局對象的屬性
第三章.變量的解構賦值
(1)數組的解構賦值
(2)對象的解構賦值
(3)字符串的解構賦值
(4)數值和布爾值的解構賦值
(5)函數參數的解構賦值
(6)圓括號問題
(7)用途
第四章.字符串的擴展
(1)字符的Unicode表示法
(2)codePointAt()
(3)String.fromCodePoint()
(4)字符串的遍歷器接口
(5)at()
(6)normalize()
(7)includes(), startsWith(), endsWith()
(8)repeat()
(9)padStart(),padEnd()
(10)模板字符串
(11)實例:模板編譯
(12)標籤模板
(13)String.raw()
第五章.正則的擴展
(1)RegExp構造函數
(2)字符串的正則方法
(3)u修飾符
(4)y修飾符
(5)sticky屬性
(6)flags屬性
(7)RegExp.escape()
(8)後行斷言
第六章.數值的擴展
(1)二進制和八進制表示法
(2)Number.isFinite(), Number.isNaN()
(3)Number.parseInt(), Number.parseFloat()
(4)Number.isInteger()
(5)Number.EPSILON
(6)安全整數和Number.isSafeInteger()
(7)Math對象的擴展
(8)指數運算符
第七章.數組的擴展
(1)Array.from()
(2)Array.of()
(3)數組實例的copyWithin()
(4)數組實例的find()和findIndex()
(5)數組實例的fill()
(6)數組實例的entries(),keys()和values()
(7)數組實例的includes()
(8)數組的空位
第八章.函數的擴展
(1)函數參數的默認值
(2)rest參數
(3)擴展運算符
(4)name屬性
(5)箭頭函數
(6)函數綁定
(7)尾調用優化
(8)函數參數的尾逗號
第九章.對象的擴展
(1)屬性的簡潔表示法
(2)屬性名錶達式
(3)方法的name屬性
(4)Object.is()
(5)Object.assign()
(6)屬性的可枚舉性
(7)屬性的遍歷
(8)proto屬性,Object.setPrototypeOf(),Object.getPrototypeOf()
(9)Object.values(),Object.entries()
(10)對象的擴展運算符
(11)Object.getOwnPropertyDescriptors()
第十章.Symbol
(1)概述
(2)做爲屬性名的Symbol
(3)實例:消除魔術字符串
(4)屬性名的遍歷
(5)Symbol.for(),Symbol.keyFor()
(6)實例:模塊的 Singleton 模式
(7)內置的Symbol值
第十一章.Proxy和Reflect
(1)Proxy概述
(2)Proxy實例的方法
(3)Proxy.revocable()
(4)Reflect概述
(5)Reflect對象的方法
第十二章.二進制數組
(1)ArrayBuffer對象
(2)TypedArray視圖
(3)複合視圖
(4)DataView視圖
(5)二進制數組的應用
第十三章.Set和Map數據結構
(1)Set
(2)WeakSet
(3)Map
(4)WeakMap
第十四章.Iterator和for...of循環
(1)Iterator(遍歷器)的概念
(2)數據結構的默認Iterator接口
(3)調用Iterator接口的場合
(4)字符串的Iterator接口
(5)Iterator接口與Generator函數
(6)遍歷器對象的return(),throw()
(7)for...of循環
第十五章.Generator 函數
(1)簡介
(2)next方法的參數
(3)for...of循環
(4)Generator.prototype.throw()
(5)Generator.prototype.return()
(6)yield*語句
(7)做爲對象屬性的Generator函數
(8)Generator函數的this
(9)含義
(10)應用
第十六章.Promise對象
(1)Promise的含義
(2)基本用法
(3)Promise.prototype.then()
(4)Promise.prototype.catch()
(5)Promise.all()
(6)Promise.race()
(7)Promise.resolve()
(8)Promise.reject()
(9)兩個有用的附加方法
(10)應用
第十七章.異步操做和Async函數
(1)基本概念
(2)Generator函數
(3)Thunk函數
(4)co模塊
(5)async函數
第十八章.Class
(1)Class基本語法
(2)Class的繼承
(3)原生構造函數的繼承
(4)Class的取值函數(getter)和存值函數(setter)
(5)Class的Generator方法
(6)Class的靜態方法
(7)Class的靜態屬性和實例屬性
(8)new.target屬性
(9)Mixin模式的實現
第十九章.修飾器
(1)類的修飾
(2)方法的修飾
(3)爲何修飾器不能用於函數?
(4)core-decorators.js
(5)使用修飾器實現自動發佈事件
(6)Mixin
(7)Trait
(8)Babel轉碼器的支持
第二十章.Module
(1)嚴格模式
(2)export命令
(3)import命令
(4)模塊的總體加載
(5)export default命令
(6)模塊的繼承
(7)ES6模塊加載的實質
(8)循環加載
(9)跨模塊常量
(10)ES6模塊的轉碼
第二十一章.編程風格
(1)塊級做用域
(2)字符串
(3)解構賦值
(4)對象
(5)數組
(6)函數
(7)Map結構
(8)Class
(9)模塊
(10)ESLint的使用
第二十二章.讀懂 ECMAScript 規格
(1)概述
(2)相等運算符
(3)數組的空位
(4)數組的map方法
1 ECMAScript 6簡介
ECMAScript 6.0(如下簡稱ES6)是JavaScript語言的下一代標準,已經在2015年6月正式發佈了。它的目標,是使得JavaScript語言能夠用來編寫復
雜的大型應用程序,成爲企業級開發語言。
標準的制定者有計劃,之後每一年發佈一次標準,使用年份做爲版本。由於ES6的第一個版本是在2015年發佈的,因此又稱ECMAScript 2015(簡稱
ES2015)。
2016年6月,小幅修訂的《ECMAScript 2016 標準》(簡稱 ES2016)如期發佈。因爲變更很是小(只新增了數組實例的includes方法和指數運算
符),所以 ES2016 與 ES2015 基本上是同一個標準,都被看做是 ES6。根據計劃,2017年6月將發佈 ES2017。
1.1 ECMAScript和JavaScript的關係
一個常見的問題是,ECMAScript和JavaScript究竟是什麼關係?
要講清楚這個問題,須要回顧歷史。1996年11月,JavaScript的創造者Netscape公司,決定將JavaScript提交給國際標準化組織ECMA,但願這種語言
可以成爲國際標準。次年,ECMA發佈262號標準文件(ECMA-262)的初版,規定了瀏覽器腳本語言的標準,並將這種語言稱爲ECMAScript,這個
版本就是1.0版。
該標準從一開始就是針對JavaScript語言制定的,可是之因此不叫JavaScript,有兩個緣由。一是商標,Java是Sun公司的商標,根據受權協議,只有
Netscape公司能夠合法地使用JavaScript這個名字,且JavaScript自己也已經被Netscape公司註冊爲商標。二是想體現這門語言的制定者是ECMA,不
是Netscape,這樣有利於保證這門語言的開放性和中立性。
所以,ECMAScript和JavaScript的關係是,前者是後者的規格,後者是前者的一種實現(另外的ECMAScript方言還有Jscript和ActionScript)。平常場
合,這兩個詞是能夠互換的。
1.2 ECMAScript的歷史
ES6從開始制定到最後發佈,整整用了15年。
前面提到,ECMAScript 1.0是1997年發佈的,接下來的兩年,連續發佈了ECMAScript 2.0(1998年6月)和ECMAScript 3.0(1999年12月)。3.0版
是一個巨大的成功,在業界獲得普遍支持,成爲通行標準,奠基了JavaScript語言的基本語法,之後的版本徹底繼承。直到今天,初學者一開始學習
JavaScript,其實就是在學3.0版的語法。
2000年,ECMAScript 4.0開始醞釀。這個版本最後沒有經過,可是它的大部份內容被ES6繼承了。所以,ES6制定的起點實際上是2000年。
爲何ES4沒有經過呢?由於這個版本太激進了,對ES3作了完全升級,致使標準委員會的一些成員不肯意接受。ECMA的第39號技術專家委員會
(Technical Committee 39,簡稱TC39)負責制訂ECMAScript標準,成員包括Microsoft、Mozilla、Google等大公司。
2007年10月,ECMAScript 4.0版草案發布,原本預計次年8月發佈正式版本。可是,各方對因而否經過這個標準,發生了嚴重分歧。以Yahoo、
Microsoft、Google爲首的大公司,反對JavaScript的大幅升級,主張小幅改動;以JavaScript創造者Brendan Eich爲首的Mozilla公司,則堅持當前的草
案。
2008年7月,因爲對於下一個版本應該包括哪些功能,各方分歧太大,爭論過於激烈,ECMA開會決定,停止ECMAScript 4.0的開發,將其中涉及現有
功能改善的一小部分,發佈爲ECMAScript 3.1,而將其餘激進的設想擴大範圍,放入之後的版本,因爲會議的氣氛,該版本的項目代號起名爲
Harmony(和諧)。會後不久,ECMAScript 3.1就更名爲ECMAScript 5。
2009年12月,ECMAScript 5.0版正式發佈。Harmony項目則一分爲二,一些較爲可行的設想定名爲JavaScript.next繼續開發,後來演變成ECMAScript
6;一些不是很成熟的設想,則被視爲JavaScript.next.next,在更遠的未來再考慮推出。TC39委員會的整體考慮是,ES5與ES3基本保持兼容,較大的
語法修正和新功能加入,將由JavaScript.next完成。當時,JavaScript.next指的是ES6,第六版發佈之後,就指ES7。TC39的判斷是,ES5會在2013年
的年中成爲JavaScript開發的主流標準,並在此後五年中一直保持這個位置。
2011年6月,ECMAscript 5.1版發佈,而且成爲ISO國際標準(ISO/IEC 16262:2011)。
2013年3月,ECMAScript 6草案凍結,再也不添加新功能。新的功能設想將被放到ECMAScript 7。
2013年12月,ECMAScript 6草案發布。而後是12個月的討論期,聽取各方反饋。
2015年6月,ECMAScript 6正式經過,成爲國際標準。從2000年算起,這時已通過去了15年。
1.3 部署進度
各大瀏覽器的最新版本,對ES6的支持能夠查看kangax.github.io/es5-compat-table/es6/。隨着時間的推移,支持度已經愈來愈高了,ES6的大部分特
性都實現了。
Node.js是JavaScript語言的服務器運行環境,對ES6的支持度比瀏覽器更高。經過Node,能夠體驗更多ES6的特性。建議使用版本管理工具nvm,來安
裝Node,由於能夠自由切換版本。不過,nvm不支持Windows系統,若是你使用Windows系統,下面的操做能夠改用nvmw或nvm-windows代替。
安裝nvm須要打開命令行窗口,運行下面的命令。
安裝nvm須要打開命令行窗口,運行下面的命令。
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/<version number>/install.sh | bash
上面命令的version number處,須要用版本號替換。本節寫做時的版本號是v0.29.0。該命令運行後,nvm會默認安裝在用戶主目錄的.nvm子目錄。
而後,激活nvm。
$ source ~/.nvm/nvm.sh
激活之後,安裝Node的最新版。
$ nvm install node
安裝完成後,切換到該版本。
$ nvm use node
使用下面的命令,能夠查看Node全部已經實現的ES6特性。
$ node --v8-options | grep harmony
--harmony_typeof
--harmony_scoping
--harmony_modules
--harmony_symbols
--harmony_proxies
--harmony_collections
--harmony_observation
--harmony_generators
--harmony_iteration
--harmony_numeric_literals
--harmony_strings
--harmony_arrays
--harmony_maths
--harmony
上面命令的輸出結果,會由於版本的不一樣而有所不一樣。
我寫了一個ES-Checker模塊,用來檢查各類運行環境對ES6的支持狀況。訪問ruanyf.github.io/es-checker,能夠看到您的瀏覽器支持ES6的程度。運
行下面的命令,能夠查看你正在使用的Node環境對ES6的支持程度。
$ npm install -g es-checker
$ es-checker
=========================================
Passes 24 feature Dectations
Your runtime supports 57% of ECMAScript 6
=========================================
1.4 Babel轉碼器
Babel是一個普遍使用的ES6轉碼器,能夠將ES6代碼轉爲ES5代碼,從而在現有環境執行。這意味着,你能夠用ES6的方式編寫程序,又不用擔憂現
有環境是否支持。下面是一個例子。
// 轉碼前
input.map(item => item + 1);
// 轉碼後
input.map(function (item) {
return item + 1;
});
上面的原始代碼用了箭頭函數,這個特性尚未獲得普遍支持,Babel將其轉爲普通函數,就能在現有的JavaScript環境執行了。
1.4.1 配置文件.babelrc
Babel的配置文件是.babelrc,存放在項目的根目錄下。使用Babel的第一步,就是配置這個文件。
該文件用來設置轉碼規則和插件,基本格式以下。
{
"presets": [],
"plugins": []
}
presets字段設定轉碼規則,官方提供如下的規則集,你能夠根據須要安裝。
# ES2015轉碼規則
$ npm install --save-dev babel-preset-es2015
# react轉碼規則
$ npm install --save-dev babel-preset-react
# ES7不一樣階段語法提案的轉碼規則(共有4個階段),選裝一個
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3
而後,將這些規則加入.babelrc。
{
"presets": [
"es2015",
"react",
"stage-2"
],
"plugins": []
}
注意,如下全部Babel工具和模塊的使用,都必須先寫好.babelrc。
1.4.2 命令行轉碼babel-cli
Babel提供babel-cli工具,用於命令行轉碼。
它的安裝命令以下。
$ npm install --global babel-cli
基本用法以下。
# 轉碼結果輸出到標準輸出
$ babel example.js
# 轉碼結果寫入一個文件
# --out-file 或 -o 參數指定輸出文件
$ babel example.js --out-file compiled.js
# 或者
$ babel example.js -o compiled.js
# 整個目錄轉碼
# --out-dir 或 -d 參數指定輸出目錄
$ babel src --out-dir lib
# 或者
$ babel src -d lib
# -s 參數生成source map文件
$ babel src -d lib -s
上面代碼是在全局環境下,進行Babel轉碼。這意味着,若是項目要運行,全局環境必須有Babel,也就是說項目產生了對環境的依賴。另外一方面,這
樣作也沒法支持不一樣項目使用不一樣版本的Babel。
一個解決辦法是將babel-cli安裝在項目之中。
# 安裝
$ npm install --save-dev babel-cli
而後,改寫package.json。
{
// ...
"devDependencies": {
"babel-cli": "^6.0.0"
},
"scripts": {
"build": "babel src -d lib"
},
}
轉碼的時候,就執行下面的命令。
$ npm run build
1.4.3 babel-node
babel-cli工具自帶一個babel-node命令,提供一個支持ES6的REPL環境。它支持Node的REPL環境的全部功能,並且能夠直接運行ES6代碼。
它不用單獨安裝,而是隨babel-cli一塊兒安裝。而後,執行babel-node就進入REPL環境。
$ babel-node
> (x => x * 2)(1)
2
babel-node命令能夠直接運行ES6腳本。將上面的代碼放入腳本文件es6.js,而後直接運行。
$ babel-node es6.js
2
babel-node也能夠安裝在項目中。
$ npm install --save-dev babel-cli
而後,改寫package.json。
{
"scripts": {
"script-name": "babel-node script.js"
}
}
上面代碼中,使用babel-node替代node,這樣script.js自己就不用作任何轉碼處理。
1.4.4 babel-register
babel-register模塊改寫require命令,爲它加上一個鉤子。此後,每當使用require加載.js、.jsx、.es和.es6後綴名的文件,就會先用Babel進行
轉碼。
$ npm install --save-dev babel-register
使用時,必須首先加載babel-register。
require("babel-register");
require("./index.js");
而後,就不須要手動對index.js轉碼了。
須要注意的是,babel-register只會對require命令加載的文件轉碼,而不會對當前文件轉碼。另外,因爲它是實時轉碼,因此只適合在開發環境使
用。
1.4.5 babel-core
若是某些代碼須要調用Babel的API進行轉碼,就要使用babel-core模塊。
安裝命令以下。
$ npm install babel-core --save
而後,在項目中就能夠調用babel-core。
var babel = require('babel-core');
// 字符串轉碼
babel.transform('code();', options);
// => { code, map, ast }
// 文件轉碼(異步)
babel.transformFile('filename.js', options, function(err, result) {
result; // => { code, map, ast }
});
// 文件轉碼(同步)
babel.transformFileSync('filename.js', options);
// => { code, map, ast }
// Babel AST轉碼
babel.transformFromAst(ast, code, options);
// => { code, map, ast }
配置對象options,能夠參看官方文檔http://babeljs.io/docs/usage/options/。
下面是一個例子。
var es6Code = 'let x = n => n + 1';
var es5Code = require('babel-core')
.transform(es6Code, {
presets: ['es2015']
})
.code;
// '"use strict";\n\nvar x = function x(n) {\n return n + 1;\n};'
上面代碼中,transform方法的第一個參數是一個字符串,表示須要被轉換的ES6代碼,第二個參數是轉換的配置對象。
1.4.6 babel-polyfill
Babel默認只轉換新的JavaScript句法(syntax),而不轉換新的API,好比Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全
局對象,以及一些定義在全局對象上的方法(好比Object.assign)都不會轉碼。
舉例來講,ES6在Array對象上新增了Array.from方法。Babel就不會轉碼這個方法。若是想讓這個方法運行,必須使用babel-polyfill,爲當前環境
提供一個墊片。
安裝命令以下。
$ npm install --save babel-polyfill
而後,在腳本頭部,加入以下一行代碼。
import 'babel-polyfill';
// 或者
require('babel-polyfill');
Babel默認不轉碼的API很是多,詳細清單能夠查看babel-plugin-transform-runtime模塊的definitions.js文件。
1.4.7 瀏覽器環境
Babel也能夠用於瀏覽器環境。可是,從Babel 6.0開始,再也不直接提供瀏覽器版本,而是要用構建工具構建出來。若是你沒有或不想使用構建工具,可
以經過安裝5.x版本的babel-core模塊獲取。
$ npm install babel-core@5
運行上面的命令之後,就能夠在當前目錄的node_modules/babel-core/子目錄裏面,找到babel的瀏覽器版本browser.js(未精簡)
和browser.min.js(已精簡)。
而後,將下面的代碼插入網頁。
<script src="node_modules/babel-core/browser.js"></script>
<script type="text/babel">
// Your ES6 code
</script>
上面代碼中,browser.js是Babel提供的轉換器腳本,能夠在瀏覽器運行。用戶的ES6腳本放在script標籤之中,可是要註明type="text/babel"。
另外一種方法是使用babel-standalone模塊提供的瀏覽器版本,將其插入網頁。
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>
注意,網頁中實時將ES6代碼轉爲ES5,對性能會有影響。生產環境須要加載已經轉碼完成的腳本。
下面是如何將代碼打包成瀏覽器能夠使用的腳本,以Babel配合Browserify爲例。首先,安裝babelify模塊。
$ npm install --save-dev babelify babel-preset-es2015
而後,再用命令行轉換ES6腳本。
$ browserify script.js -o bundle.js \
-t [ babelify --presets [ es2015 ] ]
上面代碼將ES6腳本script.js,轉爲bundle.js,瀏覽器直接加載後者就能夠了。
在package.json設置下面的代碼,就不用每次命令行都輸入參數了。
{
"browserify": {
"transform": [["babelify", { "presets": ["es2015"] }]]
}
}
1.4.8 在線轉換
Babel提供一個REPL在線編譯器,能夠在線將ES6代碼轉爲ES5代碼。轉換後的代碼,能夠直接做爲ES5代碼插入網頁運行。
1.4.9 與其餘工具的配合
許多工具須要Babel進行前置轉碼,這裏舉兩個例子:ESLint和Mocha。
ESLint用於靜態檢查代碼的語法和風格,安裝命令以下。
$ npm install --save-dev eslint babel-eslint
而後,在項目根目錄下,新建一個配置文件.eslintrc,在其中加入parser字段。
{
"parser": "babel-eslint",
"rules": {
...
}
}
再在package.json之中,加入相應的scripts腳本。
{
"name": "my-module",
"scripts": {
"lint": "eslint my-files.js"
},
"devDependencies": {
"babel-eslint": "...",
"eslint": "..."
}
}
Mocha則是一個測試框架,若是須要執行使用ES6語法的測試腳本,能夠修改package.json的scripts.test。
"scripts": {
"test": "mocha --ui qunit --compilers js:babel-core/register"
}
上面命令中,--compilers參數指定腳本的轉碼器,規定後綴名爲js的文件,都須要使用babel-core/register先轉碼。
1.5 Traceur轉碼器
Google公司的Traceur轉碼器,也能夠將ES6代碼轉爲ES5代碼。
1.5.1 直接插入網頁
Traceur容許將ES6代碼直接插入網頁。首先,必須在網頁頭部加載Traceur庫文件。
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
<script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
<script type="module">
import './Greeter.js';
</script>
上面代碼中,一共有4個script標籤。第一個是加載Traceur的庫文件,第二個和第三個是將這個庫文件用於瀏覽器環境,第四個則是加載用戶腳本,
這個腳本里面能夠使用ES6代碼。
注意,第四個script標籤的type屬性的值是module,而不是text/javascript。這是Traceur編譯器識別ES6代碼的標誌,編譯器會自動將所
有type=module的代碼編譯爲ES5,而後再交給瀏覽器執行。
除了引用外部ES6腳本,也能夠直接在網頁中放置ES6代碼。
<script type="module">
class Calc {
constructor(){
console.log('Calc constructor');
}
add(a, b){
return a + b;
}
}
var c = new Calc();
console.log(c.add(4,5));
</script>
正常狀況下,上面代碼會在控制檯打印出9。
若是想對Traceur的行爲有精確控制,能夠採用下面參數配置的寫法。
<script>
// Create the System object
window.System = new traceur.runtime.BrowserTraceurLoader();
// Set some experimental options
var metadata = {
traceurOptions: {
experimental: true,
properTailCalls: true,
symbols: true,
arrayComprehension: true,
asyncFunctions: true,
asyncGenerators: exponentiation,
forOn: true,
generatorComprehension: true
}
};
// Load your module
System.import('./myModule.js', {metadata: metadata}).catch(function(ex) {
console.error('Import failed', ex.stack || ex);
});
</script>
上面代碼中,首先生成Traceur的全局對象window.System,而後System.import方法能夠用來加載ES6模塊。加載的時候,須要傳入一個配置對
象metadata,該對象的traceurOptions屬性能夠配置支持ES6功能。若是設爲experimental: true,就表示除了ES6之外,還支持一些實驗性的新功
能。
1.5.2 在線轉換
Traceur也提供一個在線編譯器,能夠在線將ES6代碼轉爲ES5代碼。轉換後的代碼,能夠直接做爲ES5代碼插入網頁運行。
上面的例子轉爲ES5代碼運行,就是下面這個樣子。
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
<script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
<script>
$traceurRuntime.ModuleStore.getAnonymousModule(function() {
"use strict";
var Calc = function Calc() {
console.log('Calc constructor');
};
($traceurRuntime.createClass)(Calc, {add: function(a, b) {
return a + b;
}}, {});
var c = new Calc();
console.log(c.add(4, 5));
return {};
});
</script>
1.5.3 命令行轉換
做爲命令行工具使用時,Traceur是一個Node的模塊,首先須要用Npm安裝。
$ npm install -g traceur
安裝成功後,就能夠在命令行下使用Traceur了。
Traceur直接運行es6腳本文件,會在標準輸出顯示運行結果,之前面的calc.js爲例。
$ traceur calc.js
Calc constructor
9
若是要將ES6腳本轉爲ES5保存,要採用下面的寫法。
$ traceur --script calc.es6.js --out calc.es5.js
上面代碼的--script選項表示指定輸入文件,--out選項表示指定輸出文件。
爲了防止有些特性編譯不成功,最好加上--experimental選項。
$ traceur --script calc.es6.js --out calc.es5.js --experimental
命令行下轉換生成的文件,就能夠直接放到瀏覽器中運行。
1.5.4 Node.js環境的用法
Traceur的Node.js用法以下(假定已安裝traceur模塊)。
var traceur = require('traceur');
var fs = require('fs');
// 將ES6腳本轉爲字符串
var contents = fs.readFileSync('es6-file.js').toString();
var result = traceur.compile(contents, {
filename: 'es6-file.js',
sourceMap: true,
// 其餘設置
modules: 'commonjs'
});
if (result.error)
throw result.error;
// result對象的js屬性就是轉換後的ES5代碼
fs.writeFileSync('out.js', result.js);
// sourceMap屬性對應map文件
fs.writeFileSync('out.js.map', result.sourceMap);
1.6 ECMAScript 7
2013年3月,ES6的草案封閉,再也不接受新功能了。新的功能將被加入ES7。
任何人均可以向TC39提案,從提案到變成正式標準,須要經歷五個階段。每一個階段的變更都須要由TC39委員會批准。
Stage 0 - Strawman(展現階段)
Stage 1 - Proposal(徵求意見階段)
Stage 2 - Draft(草案階段)
Stage 3 - Candidate(候選人階段)
Stage 4 - Finished(定案階段)
一個提案只要能進入Stage 2,就差很少等於確定會包括在ES7裏面。
本書的寫做目標之一,是跟蹤ECMAScript語言的最新進展。對於那些明確的、或者頗有但願列入ES7的功能,尤爲是那些Babel已經支持的功能,都將
予以介紹。
本書介紹的ES7功能清單以下。
Stage 0:
Function Bind Syntax:函數的綁定運算符
String.prototype.at:字符串的靜態方法at
Stage 1:
Class and Property Decorators:Class的修飾器
Class Property Declarations:Class的屬性聲明
Additional export-from Statements:export的寫法改進
String.prototype.{trimLeft,trimRight}:字符串刪除頭尾空格的方法
Stage 2:
Rest/Spread Properties:對象的Rest參數和擴展運算符
Stage 3
SIMD API:「單指令,多數據」命令集
Async Functions:async函數
Object.values/Object.entries:Object的靜態方法values()和entries()
String padding:字符串長度補全
Trailing commas in function parameter lists and calls:函數參數的尾逗號
Object.getOwnPropertyDescriptors:Object的靜態方法getOwnPropertyDescriptors
Stage 4:
Array.prototype.includes:數組實例的includes方法
Exponentiation Operator:指數運算符
ECMAScript當前的全部提案,能夠在TC39的官方網站Github.com/tc39/ecma262查看。
Babel轉碼器能夠經過安裝和使用插件來使用各個stage的語法。
2 let和const命令
2.1 let命令
2.1.1 基本用法
ES6新增了let命令,用來聲明變量。它的用法相似於var,可是所聲明的變量,只在let命令所在的代碼塊內有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
上面代碼在代碼塊之中,分別用let和var聲明瞭兩個變量。而後在代碼塊以外調用這兩個變量,結果let聲明的變量報錯,var聲明的變量返回了正確
的值。這代表,let聲明的變量只在它所在的代碼塊有效。
for循環的計數器,就很合適使用let命令。
for (let i = 0; i < arr.length; i++) {}
console.log(i);
//ReferenceError: i is not defined
上面代碼的計數器i,只在for循環體內有效。
下面的代碼若是使用var,最後輸出的是10。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代碼中,變量i是var聲明的,在全局範圍內都有效。因此每一次循環,新的i值都會覆蓋舊值,致使最後輸出的是最後一輪的i的值。
若是使用let,聲明的變量僅在塊級做用域內有效,最後輸出的是6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代碼中,變量i是let聲明的,當前的i只在本輪循環有效,因此每一次循環的i其實都是一個新的變量,因此最後輸出的是6。
2.1.2 不存在變量提高
let不像var那樣會發生「變量提高」現象。因此,變量必定要在聲明後使用,不然報錯。
console.log(foo); // 輸出undefined
console.log(bar); // 報錯ReferenceError
var foo = 2;
let bar = 2;
上面代碼中,變量foo用var命令聲明,會發生變量提高,即腳本開始運行時,變量foo已經存在了,可是沒有值,因此會輸出undefined。變
量bar用let命令聲明,不會發生變量提高。這表示在聲明它以前,變量bar是不存在的,這時若是用到它,就會拋出一個錯誤。
2.1.3 暫時性死區
只要塊級做用域內存在let命令,它所聲明的變量就「綁定」(binding)這個區域,再也不受外部的影響。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代碼中,存在全局變量tmp,可是塊級做用域內let又聲明瞭一個局部變量tmp,致使後者綁定這個塊級做用域,因此在let聲明變量前,對tmp賦
值會報錯。
ES6明確規定,若是區塊中存在let和const命令,這個區塊對這些命令聲明的變量,從一開始就造成了封閉做用域。凡是在聲明以前就使用這些變
量,就會報錯。
總之,在代碼塊內,使用let命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱TDZ)。
if (true) {
// TDZ開始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ結束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代碼中,在let命令聲明變量tmp以前,都屬於變量tmp的「死區」。
「暫時性死區」也意味着typeof再也不是一個百分之百安全的操做。
typeof x; // ReferenceError
let x;
上面代碼中,變量x使用let命令聲明,因此在聲明以前,都屬於x的「死區」,只要用到該變量就會報錯。所以,typeof運行時就會拋出一
個ReferenceError。
做爲比較,若是一個變量根本沒有被聲明,使用typeof反而不會報錯。
typeof undeclared_variable // "undefined"
上面代碼中,undeclared_variable是一個不存在的變量名,結果返回「undefined」。因此,在沒有let以前,typeof運算符是百分之百安全的,永遠不
會報錯。如今這一點不成立了。這樣的設計是爲了讓你們養成良好的編程習慣,變量必定要在聲明以後使用,不然就報錯。
有些「死區」比較隱蔽,不太容易發現。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 報錯
上面代碼中,調用bar函數之因此報錯(某些實現可能不報錯),是由於參數x默認值等於另外一個參數y,而此時y尚未聲明,屬於」死區「。若是y的默
認值是x,就不會報錯,由於此時x已經聲明瞭。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
ES6規定暫時性死區和let、const語句不出現變量提高,主要是爲了減小運行時錯誤,防止在變量聲明前就使用這個變量,從而致使意料以外的行
爲。這樣的錯誤在ES5是很常見的,如今有了這種規定,避免此類錯誤就很容易了。
總之,暫時性死區的本質就是,只要一進入當前做用域,所要使用的變量就已經存在了,可是不可獲取,只有等到聲明變量的那一行代碼出現,纔可
以獲取和使用該變量。
2.1.4 不容許重複聲明
let不容許在相同做用域內,重複聲明同一個變量。
// 報錯
function () {
let a = 10;
var a = 1;
}
// 報錯
// 報錯
function () {
let a = 10;
let a = 1;
}
所以,不能在函數內部從新聲明參數。
function func(arg) {
let arg; // 報錯
}
function func(arg) {
{
let arg; // 不報錯
}
}
2.2 塊級做用域
2.2.1 爲何須要塊級做用域?
ES5只有全局做用域和函數做用域,沒有塊級做用域,這帶來不少不合理的場景。
第一種場景,內層變量可能會覆蓋外層變量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = "hello world";
}
}
f(); // undefined
上面代碼中,函數f執行後,輸出結果爲undefined,緣由在於變量提高,致使內層的tmp變量覆蓋了外層的tmp變量。
第二種場景,用來計數的循環變量泄露爲全局變量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代碼中,變量i只用來控制循環,可是循環結束後,它並無消失,泄露成了全局變量。
2.2.2 ES6的塊級做用域
let實際上爲JavaScript新增了塊級做用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函數有兩個代碼塊,都聲明瞭變量n,運行後輸出5。這表示外層代碼塊不受內層代碼塊的影響。若是使用var定義變量n,最後輸出的值就是
10。
ES6容許塊級做用域的任意嵌套。
{{{{{let insane = 'Hello World'}}}}};
上面代碼使用了一個五層的塊級做用域。外層做用域沒法讀取內層做用域的變量。
{{{{
{let insane = 'Hello World'}
console.log(insane); // 報錯
}}}};
內層做用域能夠定義外層做用域的同名變量。
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
塊級做用域的出現,實際上使得得到普遍應用的當即執行匿名函數(IIFE)再也不必要了。
// IIFE寫法
(function () {
var tmp = ...;
...
}());
// 塊級做用域寫法
{
let tmp = ...;
...
}
2.2.3 塊級做用域與函數聲明
函數能不能在塊級做用域之中聲明,是一個至關使人混淆的問題。
ES5規定,函數只能在頂層做用域和函數做用域之中聲明,不能在塊級做用域聲明。
// 狀況一
if (true) {
function f() {}
}
// 狀況二
try {
function f() {}
} catch(e) {
}
上面代碼的兩種函數聲明,根據ES5的規定都是非法的。
可是,瀏覽器沒有遵照這個規定,仍是支持在塊級做用域之中聲明函數,所以上面兩種狀況實際都能運行,不會報錯。不過,「嚴格模式」下仍是會報
錯。
// ES5嚴格模式
'use strict';
if (true) {
function f() {}
}
// 報錯
ES6引入了塊級做用域,明確容許在塊級做用域之中聲明函數。
// ES6嚴格模式
'use strict';
if (true) {
function f() {}
}
// 不報錯
而且ES6規定,塊級做用域之中,函數聲明語句的行爲相似於let,在塊級做用域以外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重複聲明一次函數f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代碼在ES5中運行,會獲得「I am inside!」,由於在if內聲明的函數f會被提高到函數頭部,實際運行的代碼以下。
// ES5版本
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
ES6的運行結果就徹底不同了,會獲得「I am outside!」。由於塊級做用域內聲明的函數相似於let,對做用域以外沒有影響,實際運行的代碼以下。
// ES6版本
function f() { console.log('I am outside!'); }
(function () {
f();
}());
很顯然,這種行爲差別會對老代碼產生很大影響。爲了減輕所以產生的不兼容問題,ES6在附錄B裏面規定,瀏覽器的實現能夠不遵照上面的規定,有
本身的行爲方式。
容許在塊級做用域內聲明函數。
函數聲明相似於var,即會提高到全局做用域或函數做用域的頭部。
同時,函數聲明還會提高到所在的塊級做用域的頭部。
注意,上面三條規則只對ES6的瀏覽器實現有效,其餘環境的實現不用遵照,仍是將塊級做用域的函數聲明看成let處理。
前面那段代碼,在Chrome環境下運行會報錯。
// ES6的瀏覽器環境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重複聲明一次函數f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
上面的代碼報錯,是由於實際運行的是下面的代碼。
// ES6的瀏覽器環境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
考慮到環境致使的行爲差別太大,應該避免在塊級做用域內聲明函數。若是確實須要,也應該寫成函數表達式,而不是函數聲明語句。
// 函數聲明語句
{
let a = 'secret';
function f() {
return a;
}
}
// 函數表達式
{
let a = 'secret';
let f = function () {
return a;
};
}
另外,還有一個須要注意的地方。ES6的塊級做用域容許聲明函數的規則,只在使用大括號的狀況下成立,若是沒有使用大括號,就會報錯。
// 不報錯
'use strict';
if (true) {
function f() {}
}
// 報錯
'use strict';
if (true)
function f() {}
2.3 const命令
const聲明一個只讀的常量。一旦聲明,常量的值就不能改變。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
上面代碼代表改變常量的值會報錯。
const聲明的變量不得改變值,這意味着,const一旦聲明變量,就必須當即初始化,不能留到之後賦值。
const foo;
// SyntaxError: Missing initializer in const declaration
上面代碼表示,對於const來講,只聲明不賦值,就會報錯。
const的做用域與let命令相同:只在聲明所在的塊級做用域內有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
const命令聲明的常量也是不提高,一樣存在暫時性死區,只能在聲明的位置後面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
上面代碼在常量MAX聲明以前就調用,結果報錯。
const聲明的常量,也與let同樣不可重複聲明。
var message = "Hello!";
let age = 25;
// 如下兩行都會報錯
const message = "Goodbye!";
const age = 30;
對於複合類型的變量,變量名不指向數據,而是指向數據所在的地址。const命令只是保證變量名指向的地址不變,並不保證該地址的數據不變,因此
將一個對象聲明爲常量必須很是當心。
const foo = {};
foo.prop = 123;
foo.prop
// 123
foo = {}; // TypeError: "foo" is read-only
上面代碼中,常量foo儲存的是一個地址,這個地址指向一個對象。不可變的只是這個地址,即不能把foo指向另外一個地址,但對象自己是可變的,所
以依然能夠爲其添加新屬性。
下面是另外一個例子。
const a = [];
a.push('Hello'); // 可執行
a.length = 0; // 可執行
a = ['Dave']; // 報錯
上面代碼中,常量a是一個數組,這個數組自己是可寫的,可是若是將另外一個數組賦值給a,就會報錯。
若是真的想將對象凍結,應該使用Object.freeze方法。
const foo = Object.freeze({});
// 常規模式時,下面一行不起做用;
// 嚴格模式時,該行會報錯
foo.prop = 123;
上面代碼中,常量foo指向一個凍結的對象,因此添加新屬性不起做用,嚴格模式時還會報錯。
除了將對象自己凍結,對象的屬性也應該凍結。下面是一個將對象完全凍結的函數。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, value) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES5只有兩種聲明變量的方法:var命令和function命令。ES6除了添加let和const命令,後面章節還會提到,另外兩種聲明變量的方法:import命令
和class命令。因此,ES6一共有6種聲明變量的方法。
2.4 全局對象的屬性
全局對象是最頂層的對象,在瀏覽器環境指的是window對象,在Node.js指的是global對象。ES5之中,全局對象的屬性與全局變量是等價的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代碼中,全局對象的屬性賦值與全局變量的賦值,是同一件事。(對於Node來講,這一條只對REPL環境適用,模塊環境之中,全局變量必須顯式
聲明成global對象的屬性。)
未聲明的全局變量,自動成爲全局對象window的屬性,這被認爲是JavaScript語言最大的設計敗筆之一。這樣的設計帶來了兩個很大的問題,首先是沒
法在編譯時就報出變量未聲明的錯誤,只有運行時才能知道,其次程序員很容易不知不覺地就建立了全局變量(好比打字出錯)。另外一方面,從語義
上講,語言的頂層對象是一個有實體含義的對象,也是不合適的。
ES6爲了改變這一點,一方面規定,爲了保持兼容性,var命令和function命令聲明的全局變量,依舊是全局對象的屬性;另外一方面規定,let命
令、const命令、class命令聲明的全局變量,不屬於全局對象的屬性。也就是說,從ES6開始,全局變量將逐步與全局對象的屬性脫鉤。
var a = 1;
// 若是在Node的REPL環境,能夠寫成global.a
// 或者採用通用方法,寫成this.a
window.a // 1
let b = 1;
window.b // undefined
上面代碼中,全局變量a由var命令聲明,因此它是全局對象的屬性;全局變量b由let命令聲明,因此它不是全局對象的屬性,返回undefined。
3 變量的解構賦值
3.1 數組的解構賦值
3.1.1 基本用法
ES6容許按照必定模式,從數組和對象中提取值,對變量進行賦值,這被稱爲解構(Destructuring)。
之前,爲變量賦值,只能直接指定值。
var a = 1;
var b = 2;
var c = 3;
ES6容許寫成下面這樣。
var [a, b, c] = [1, 2, 3];
上面代碼表示,能夠從數組中提取值,按照對應位置,對變量賦值。
本質上,這種寫法屬於「模式匹配」,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。下面是一些使用嵌套數組進行解構的例子。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
若是解構不成功,變量的值就等於undefined。
var [foo] = [];
var [bar, foo] = [1];
以上兩種狀況都屬於解構不成功,foo的值都會等於undefined。
另外一種狀況是不徹底解構,即等號左邊的模式,只匹配一部分的等號右邊的數組。這種狀況下,解構依然能夠成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
上面兩個例子,都屬於不徹底解構,可是能夠成功。
若是等號的右邊不是數組(或者嚴格地說,不是可遍歷的結構,參見《Iterator》一章),那麼將會報錯。
// 報錯
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
上面的表達式都會報錯,由於等號右邊的值,要麼轉爲對象之後不具有Iterator接口(前五個表達式),要麼自己就不具有Iterator接口(最後一個表達
式)。
解構賦值不只適用於var命令,也適用於let和const命令。
var [v1, v2, ..., vN ] = array;
let [v1, v2, ..., vN ] = array;
const [v1, v2, ..., vN ] = array;
對於Set結構,也能夠使用數組的解構賦值。
let [x, y, z] = new Set(["a", "b", "c"]);
x // "a"
事實上,只要某種數據結構具備Iterator接口,均可以採用數組形式的解構賦值。
function* fibs() {
var a = 0;
var b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
var [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5
上面代碼中,fibs是一個Generator函數,原生具備Iterator接口。解構賦值會依次從這個接口獲取值。
3.1.2 默認值
解構賦值容許指定默認值。
var [foo = true] = [];
foo // true
[x, y = 'b'] = ['a']; // x='a', y='b'
[x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意,ES6內部使用嚴格相等運算符(===),判斷一個位置是否有值。因此,若是一個數組成員不嚴格等於undefined,默認值是不會生效的。
var [x = 1] = [undefined];
x // 1
var [x = 1] = [null];
x // null
上面代碼中,若是一個數組成員是null,默認值就不會生效,由於null不嚴格等於undefined。
若是默認值是一個表達式,那麼這個表達式是惰性求值的,即只有在用到的時候,纔會求值。
function f() {
console.log('aaa');
}
let [x = f()] = [1];
上面代碼中,由於x能取到值,因此函數f根本不會執行。上面的代碼其實等價於下面的代碼。
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
默認值能夠引用解構賦值的其餘變量,但該變量必須已經聲明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError
上面最後一個表達式之因此會報錯,是由於x用到默認值y時,y尚未聲明。
3.2 對象的解構賦值
解構不只能夠用於數組,還能夠用於對象。
var { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
對象的解構與數組有一個重要的不一樣。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才
能取到正確的值。
var { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
var { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
上面代碼的第一個例子,等號左邊的兩個變量的次序,與等號右邊兩個同名屬性的次序不一致,可是對取值徹底沒有影響。第二個例子的變量沒有對
應的同名屬性,致使取不到值,最後等於undefined。
若是變量名與屬性名不一致,必須寫成下面這樣。
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
這實際上說明,對象的解構賦值是下面形式的簡寫(參見《對象的擴展》一章)。
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
也就是說,對象的解構賦值的內部機制,是先找到同名屬性,而後再賦給對應的變量。真正被賦值的是後者,而不是前者。
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
上面代碼中,真正被賦值的是變量baz,而不是模式foo。
注意,採用這種寫法時,變量的聲明和賦值是一體的。對於let和const來講,變量不能從新聲明,因此一旦賦值的變量之前聲明過,就會報錯。
let foo;
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"
let baz;
let {bar: baz} = {bar: 1}; // SyntaxError: Duplicate declaration "baz"
上面代碼中,解構賦值的變量都會從新聲明,因此報錯了。不過,由於var命令容許從新聲明,因此這個錯誤只會在使用let和const命令時出現。若是
沒有第二個let命令,上面的代碼就不會報錯。
let foo;
({foo} = {foo: 1}); // 成功
let baz;
({bar: baz} = {bar: 1}); // 成功
上面代碼中,let命令下面一行的圓括號是必須的,不然會報錯。由於解析器會將起首的大括號,理解成一個代碼塊,而不是賦值語句。
和數組同樣,解構也能夠用於嵌套結構的對象。
var obj = {
p: [
'Hello',
{ y: 'World' }
]
};
var { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
注意,這時p是模式,不是變量,所以不會被賦值。
var node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
var { loc: { start: { line }} } = node;
line // 1
loc // error: loc is undefined
start // error: start is undefined
上面代碼中,只有line是變量,loc和start都是模式,不會被賦值。
下面是嵌套賦值的例子。
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]
對象的解構也能夠指定默認值。
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x:y = 3} = {};
y // 3
var {x:y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
默認值生效的條件是,對象的屬性值嚴格等於undefined。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
上面代碼中,若是x屬性等於null,就不嚴格相等於undefined,致使默認值不會生效。
若是解構失敗,變量的值等於undefined。
var {foo} = {bar: 'baz'};
foo // undefined
若是解構模式是嵌套的對象,並且子對象所在的父屬性不存在,那麼將會報錯。
// 報錯
var {foo: {bar}} = {baz: 'baz'};
上面代碼中,等號左邊對象的foo屬性,對應一個子對象。該子對象的bar屬性,解構時會報錯。緣由很簡單,由於foo這時等於undefined,再取子屬
性就會報錯,請看下面的代碼。
var _tmp = {baz: 'baz'};
_tmp.foo.bar // 報錯
若是要將一個已經聲明的變量用於解構賦值,必須很是當心。
// 錯誤的寫法
var x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代碼的寫法會報錯,由於JavaScript引擎會將{x}理解成一個代碼塊,從而發生語法錯誤。只有不將大括號寫在行首,避免JavaScript將其解釋爲代
碼塊,才能解決這個問題。
// 正確的寫法
({x} = {x: 1});
上面代碼將整個解構賦值語句,放在一個圓括號裏面,就能夠正確執行。關於圓括號與解構賦值的關係,參見下文。
解構賦值容許,等號左邊的模式之中,不放置任何變量名。所以,能夠寫出很是古怪的賦值表達式。
({} = [true, false]);
({} = 'abc');
({} = []);
上面的表達式雖然毫無心義,可是語法是合法的,能夠執行。
對象的解構賦值,能夠很方便地將現有對象的方法,賦值到某個變量。
let { log, sin, cos } = Math;
上面代碼將Math對象的對數、正弦、餘弦三個方法,賦值到對應的變量上,使用起來就會方便不少。
因爲數組本質是特殊的對象,所以能夠對數組進行對象屬性的解構。
var arr = [1, 2, 3];
var {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
上面代碼對數組進行對象結構。數組arr的0鍵對應的值是1,[arr.length - 1]就是2鍵,對應的值是3。方括號這種寫法,屬於「屬性名錶達式」,參
見《對象的擴展》一章。
3.3 字符串的解構賦值
字符串也能夠解構賦值。這是由於此時,字符串被轉換成了一個相似數組的對象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
相似數組的對象都有一個length屬性,所以還能夠對這個屬性解構賦值。
let {length : len} = 'hello';
len // 5
3.4 數值和布爾值的解構賦值
解構賦值時,若是等號右邊是數值和布爾值,則會先轉爲對象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代碼中,數值和布爾值的包裝對象都有toString屬性,所以變量s都能取到值。
解構賦值的規則是,只要等號右邊的值不是對象,就先將其轉爲對象。因爲undefined和null沒法轉爲對象,因此對它們進行解構賦值,都會報錯。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
3.5 函數參數的解構賦值
函數的參數也能夠使用解構賦值。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
上面代碼中,函數add的參數表面上是一個數組,但在傳入參數的那一刻,數組參數就被解構成變量x和y。對於函數內部的代碼來講,它們能感覺到的
參數就是x和y。
下面是另外一個例子。
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函數參數的解構也能夠使用默認值。
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
上面代碼中,函數move的參數是一個對象,經過對這個對象進行解構,獲得變量x和y的值。若是解構失敗,x和y等於默認值。
注意,下面的寫法會獲得不同的結果。
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
上面代碼是爲函數move的參數指定默認值,而不是爲變量x和y指定默認值,因此會獲得與前一種寫法不一樣的結果。
undefined就會觸發函數參數的默認值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
3.6 圓括號問題
解構賦值雖然很方便,可是解析起來並不容易。對於編譯器來講,一個式子究竟是模式,仍是表達式,沒有辦法從一開始就知道,必須解析到(或解
析不到)等號才能知道。
由此帶來的問題是,若是模式中出現圓括號怎麼處理。ES6的規則是,只要有可能致使解構的歧義,就不得使用圓括號。
可是,這條規則實際上不那麼容易辨別,處理起來至關麻煩。所以,建議只要有可能,就不要在模式中放置圓括號。
3.6.1 不能使用圓括號的狀況
如下三種解構賦值不得使用圓括號。
(1)變量聲明語句中,不能帶有圓括號。
// 所有報錯
var [(a)] = [1];
var {x: (c)} = {};
var ({x: c}) = {};
var {(x: c)} = {};
var {(x): c} = {};
var { o: ({ p: p }) } = { o: { p: 2 } };
上面三個語句都會報錯,由於它們都是變量聲明語句,模式不能使用圓括號。
(2)函數參數中,模式不能帶有圓括號。
函數參數也屬於變量聲明,所以不能帶有圓括號。
// 報錯
function f([(z)]) { return z; }
(3)賦值語句中,不能將整個模式,或嵌套模式中的一層,放在圓括號之中。
// 所有報錯
({ p: a }) = { p: 42 };
([a]) = [5];
上面代碼將整個模式放在圓括號之中,致使報錯。
// 報錯
[({ p: a }), { x: c }] = [{}, {}];
上面代碼將嵌套模式的一層,放在圓括號之中,致使報錯。
3.6.2 能夠使用圓括號的狀況
能夠使用圓括號的狀況只有一種:賦值語句的非模式部分,能夠使用圓括號。
[(b)] = [3]; // 正確
({ p: (d) } = {}); // 正確
[(parseInt.prop)] = [3]; // 正確
上面三行語句均可以正確執行,由於首先它們都是賦值語句,而不是聲明語句;其次它們的圓括號都不屬於模式的一部分。第一行語句中,模式是取
數組的第一個成員,跟圓括號無關;第二行語句中,模式是p,而不是d;第三行語句與第一行語句的性質一致。
3.7 用途
變量的解構賦值用途不少。
(1)交換變量的值
[x, y] = [y, x];
上面代碼交換變量x和y的值,這樣的寫法不只簡潔,並且易讀,語義很是清晰。
(2)從函數返回多個值
函數只能返回一個值,若是要返回多個值,只能將它們放在數組或對象裏返回。有了解構賦值,取出這些值就很是方便。
// 返回一個數組
function example() {
return [1, 2, 3];
}
var [a, b, c] = example();
// 返回一個對象
function example() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = example();
(3)函數參數的定義
解構賦值能夠方便地將一組參數與變量名對應起來。
// 參數是一組有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 參數是一組無次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取JSON數據
解構賦值對提取JSON對象中的數據,尤爲有用。
var jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
上面代碼能夠快速提取JSON數據的值。
(5)函數參數的默認值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
}) {
// ... do stuff
};
指定參數的默認值,就避免了在函數體內部再寫var foo = config.foo || 'default foo';這樣的語句。
(6)遍歷Map結構
任何部署了Iterator接口的對象,均可以用for...of循環遍歷。Map結構原生支持Iterator接口,配合變量的解構賦值,獲取鍵名和鍵值就很是方便。
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
若是隻想獲取鍵名,或者只想獲取鍵值,能夠寫成下面這樣。
// 獲取鍵名
for (let [key] of map) {
// ...
}
// 獲取鍵值
for (let [,value] of map) {
// ...
}
(7)輸入模塊的指定方法
加載模塊時,每每須要指定輸入那些方法。解構賦值使得輸入語句很是清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
4 字符串的擴展
ES6增強了對Unicode的支持,而且擴展了字符串對象。
4.1字符的Unicode表示法
JavaScript容許採用\uxxxx形式表示一個字符,其中「xxxx」表示字符的碼點。
"\u0061"
// "a"
可是,這種表示法只限於\u0000——\uFFFF之間的字符。超出這個範圍的字符,必須用兩個雙字節的形式表達。
"\uD842\uDFB7"
// ""
"\u20BB7"
// " 7"
上面代碼表示,若是直接在「\u」後面跟上超過0xFFFF的數值(好比\u20BB7),JavaScript會理解成「\u20BB+7」。因爲\u20BB是一個不可打印字符,所
以只會顯示一個空格,後面跟着一個7。
ES6對這一點作出了改進,只要將碼點放入大括號,就能正確解讀該字符。
"\u{20BB7}"
// ""
"\u{41}\u{42}\u{43}"
// "ABC"
let hello = 123;
hell\u{6F} // 123
'\u{1F680}' === '\uD83D\uDE80'
// true
上面代碼中,最後一個例子代表,大括號表示法與四字節的UTF-16編碼是等價的。
有了這種表示法以後,JavaScript共有6種方法能夠表示一個字符。
'\z' === 'z' // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true
4.2 codePointAt()
JavaScript內部,字符以UTF-16的格式儲存,每一個字符固定爲2個字節。對於那些須要4個字節儲存的字符(Unicode碼點大於0xFFFF的字
符),JavaScript會認爲它們是兩個字符。
var s = "";
s.length // 2
s.charAt(0) // ''
s.charAt(1) // ''
s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271
上面代碼中,漢字「」的碼點是0x20BB7,UTF-16編碼爲0xD842 0xDFB7(十進制爲55362 57271),須要4個字節儲存。對於這種4個字節的字
符,JavaScript不能正確處理,字符串長度會誤判爲2,並且charAt方法沒法讀取整個字符,charCodeAt方法只能分別返回前兩個字節和後兩個字節的
值。
ES6提供了codePointAt方法,可以正確處理4個字節儲存的字符,返回一個字符的碼點。
var s = 'a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.charCodeAt(2) // 97
codePointAt方法的參數,是字符在字符串中的位置(從0開始)。上面代碼中,JavaScript將「a」視爲三個字符,codePointAt方法在第一個字符上,
正確地識別了「」,返回了它的十進制碼點134071(即十六進制的20BB7)。在第二個字符(即「」的後兩個字節)和第三個字
符「a」上,codePointAt方法的結果與charCodeAt方法相同。
總之,codePointAt方法會正確返回32位的UTF-16字符的碼點。對於那些兩個字節儲存的常規字符,它的返回結果與charCodeAt方法相同。
codePointAt方法返回的是碼點的十進制值,若是想要十六進制的值,能夠使用toString方法轉換一下。
var s = 'a';
s.codePointAt(0).toString(16) // "20bb7"
s.charCodeAt(2).toString(16) // "61"
你可能注意到了,codePointAt方法的參數,仍然是不正確的。好比,上面代碼中,字符a在字符串s的正確位置序號應該是1,可是必須
向charCodeAt方法傳入2。解決這個問題的一個辦法是使用for...of循環,由於它會正確識別32位的UTF-16字符。
var s = 'a';
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61
codePointAt方法是測試一個字符由兩個字節仍是由四個字節組成的最簡單方法。
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit("") // true
is32Bit("a") // false
4.3 String.fromCodePoint()
ES5提供String.fromCharCode方法,用於從碼點返回對應字符,可是這個方法不能識別32位的UTF-16字符(Unicode編號大於0xFFFF)。
String.fromCharCode(0x20BB7)
// "ஷ"
上面代碼中,String.fromCharCode不能識別大於0xFFFF的碼點,因此0x20BB7就發生了溢出,最高位2被捨棄了,最後返回碼點U+0BB7對應的字符,
而不是碼點U+20BB7對應的字符。
ES6提供了String.fromCodePoint方法,能夠識別0xFFFF的字符,彌補了String.fromCharCode方法的不足。在做用上,正好與codePointAt方法相
反。
String.fromCodePoint(0x20BB7)
// ""
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true
上面代碼中,若是String.fromCodePoint方法有多個參數,則它們會被合併成一個字符串返回。
注意,fromCodePoint方法定義在String對象上,而codePointAt方法定義在字符串的實例對象上。
4.4 字符串的遍歷器接口
ES6爲字符串添加了遍歷器接口(詳見《Iterator》一章),使得字符串能夠被for...of循環遍歷。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
除了遍歷字符串,這個遍歷器最大的優勢是能夠識別大於0xFFFF的碼點,傳統的for循環沒法識別這樣的碼點。
var text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "
for (let i of text) {
console.log(i);
}
// ""
上面代碼中,字符串text只有一個字符,可是for循環會認爲它包含兩個字符(都不可打印),而for...of循環會正確識別出這一個字符。
4.5 at()
ES5對字符串對象提供charAt方法,返回字符串給定位置的字符。該方法不能識別碼點大於0xFFFF的字符。
'abc'.charAt(0) // "a"
''.charAt(0) // "\uD842"
上面代碼中,charAt方法返回的是UTF-16編碼的第一個字節,其實是沒法顯示的。
目前,有一個提案,提出字符串實例的at方法,能夠識別Unicode編號大於0xFFFF的字符,返回正確的字符。
'abc'.at(0) // "a"
''.at(0) // ""
這個方法能夠經過墊片庫實現。
4.6 normalize()
許多歐洲語言有語調符號和重音符號。爲了表示它們,Unicode提供了兩種方法。一種是直接提供帶重音符號的字符,好比Ǒ(\u01D1)。另外一種是提
供合成符號(combining character),即原字符與重音符號的合成,兩個字符合成一個字符,好比O(\u004F)和ˇ(\u030C)合
成Ǒ(\u004F\u030C)。
這兩種表示方法,在視覺和語義上都等價,可是JavaScript不能識別。
'\u01D1'==='\u004F\u030C' //false
'\u01D1'.length // 1
'\u004F\u030C'.length // 2
上面代碼表示,JavaScript將合成字符視爲兩個字符,致使兩種表示方法不相等。
ES6提供字符串實例的normalize()方法,用來將字符的不一樣表示方法統一爲一樣的形式,這稱爲Unicode正規化。
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true
normalize方法能夠接受一個參數來指定normalize的方式,參數的四個可選值以下。
NFC,默認參數,表示「標準等價合成」(Normalization Form Canonical Composition),返回多個簡單字符的合成字符。所謂「標準等價」指的是視
覺和語義上的等價。
NFD,表示「標準等價分解」(Normalization Form Canonical Decomposition),即在標準等價的前提下,返回合成字符分解的多個簡單字符。
NFKC,表示「兼容等價合成」(Normalization Form Compatibility Composition),返回合成字符。所謂「兼容等價」指的是語義上存在等價,但視覺
上不等價,好比「囍」和「喜喜」。(這只是用來舉例,normalize方法不能識別中文。)
NFKD,表示「兼容等價分解」(Normalization Form Compatibility Decomposition),即在兼容等價的前提下,返回合成字符分解的多個簡單字符。
'\u004F\u030C'.normalize('NFC').length // 1
'\u004F\u030C'.normalize('NFD').length // 2
上面代碼表示,NFC參數返回字符的合成形式,NFD參數返回字符的分解形式。
不過,normalize方法目前不能識別三個或三個以上字符的合成。這種狀況下,仍是隻能使用正則表達式,經過Unicode編號區間判斷。
4.7 includes(), startsWith(), endsWith()
傳統上,JavaScript只有indexOf方法,能夠用來肯定一個字符串是否包含在另外一個字符串中。ES6又提供了三種新方法。
includes():返回布爾值,表示是否找到了參數字符串。
startsWith():返回布爾值,表示參數字符串是否在源字符串的頭部。
endsWith():返回布爾值,表示參數字符串是否在源字符串的尾部。
var s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
這三個方法都支持第二個參數,表示開始搜索的位置。
var s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
上面代碼表示,使用第二個參數n時,endsWith的行爲與其餘兩個方法有所不一樣。它針對前n個字符,而其餘兩個方法針對從第n個位置直到字符串結
束。
4.8 repeat()
repeat方法返回一個新字符串,表示將原字符串重複n次。
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
參數若是是小數,會被取整。
'na'.repeat(2.9) // "nana"
若是repeat的參數是負數或者Infinity,會報錯。
'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError
可是,若是參數是0到-1之間的小數,則等同於0,這是由於會先進行取整運算。0到-1之間的小數,取整之後等於-0,repeat視同爲0。
'na'.repeat(-0.9) // ""
參數NaN等同於0。
'na'.repeat(NaN) // ""
若是repeat的參數是字符串,則會先轉換成數字。
'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"
4.9 padStart(),padEnd()
ES7推出了字符串補全長度的功能。若是某個字符串不夠指定長度,會在頭部或尾部補全。padStart用於頭部補全,padEnd用於尾部補全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
上面代碼中,padStart和padEnd一共接受兩個參數,第一個參數用來指定字符串的最小長度,第二個參數是用來補全的字符串。
若是原字符串的長度,等於或大於指定的最小長度,則返回原字符串。
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
若是用來補全的字符串與原字符串,二者的長度之和超過了指定的最小長度,則會截去超出位數的補全字符串。
'abc'.padStart(10, '0123456789')
// '0123456abc'
若是省略第二個參數,則會用空格補全長度。
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
padStart的常見用途是爲數值補全指定位數。下面代碼生成10位的數值字符串。
'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"
另外一個用途是提示字符串格式。
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
4.10 模板字符串
傳統的JavaScript語言,輸出模板一般是這樣寫的。
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
上面這種寫法至關繁瑣不方便,ES6引入了模板字符串解決這個問題。
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
模板字符串(template string)是加強版的字符串,用反引號(`)標識。它能夠看成普通字符串使用,也能夠用來定義多行字符串,或者在字符串中
嵌入變量。
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入變量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
上面代碼中的模板字符串,都是用反引號表示。若是在模板字符串中須要使用反引號,則前面要用反斜槓轉義。
var greeting = `\`Yo\` World!`;
若是使用模板字符串表示多行字符串,全部的空格和縮進都會被保留在輸出之中。
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`);
上面代碼中,全部模板字符串的空格和換行,都是被保留的,好比<ul>標籤前面會有一個換行。若是你不想要這個換行,能夠使用trim方法消除它。
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`.trim());
模板字符串中嵌入變量,須要將變量名寫在${}之中。
function authorize(user, action) {
if (!user.hasPrivilege(action)) {
throw new Error(
// 傳統寫法爲
// 'User '
// + user.name
// + ' is not authorized to do '
// + action
// + '.'
`User ${user.name} is not authorized to do ${action}.`);
}
}
大括號內部能夠放入任意的JavaScript表達式,能夠進行運算,以及引用對象屬性。
var x = 1;
var y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
var obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// 3
模板字符串之中還能調用函數。
function fn() {
return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar
若是大括號中的值不是字符串,將按照通常的規則轉爲字符串。好比,大括號中是一個對象,將默認調用對象的toString方法。
若是模板字符串中的變量沒有聲明,將報錯。
// 變量place沒有聲明
var msg = `Hello, ${place}`;
// 報錯
因爲模板字符串的大括號內部,就是執行JavaScript代碼,所以若是大括號內部是一個字符串,將會原樣輸出。
`Hello ${'World'}`
// "Hello World"
模板字符串甚至還能嵌套。
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join('')}
</table>
`;
上面代碼中,模板字符串的變量之中,又嵌入了另外一個模板字符串,使用方法以下。
const data = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
console.log(tmpl(data));
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
若是須要引用模板字符串自己,在須要時執行,能夠像下面這樣寫。
// 寫法一
let str = 'return ' + '`Hello ${name}!`';
let func = new Function('name', str);
func('Jack') // "Hello Jack!"
// 寫法二
let str = '(name) => `Hello ${name}!`';
let func = eval.call(null, str);
func('Jack') // "Hello Jack!"
4.11 實例:模板編譯
下面,咱們來看一個經過模板字符串,生成正式模板的實例。
var template = `
<ul>
<% for(var i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
上面代碼在模板字符串之中,放置了一個常規模板。該模板使用<%...%>放置JavaScript代碼,使用<%= ... %>輸出JavaScript表達式。
怎麼編譯這個模板字符串呢?
一種思路是將其轉換爲JavaScript表達式字符串。
echo('<ul>');
for(var i=0; i < data.supplies.length; i++) {
echo('<li>');
echo(data.supplies[i]);
echo('</li>');
};
echo('</ul>');
這個轉換使用正則表達式就好了。
var evalExpr = /<%=(.+?)%>/g;
var expr = /<%([\s\S]+?)%>/g;
template = template
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
.replace(expr, '`); \n $1 \n echo(`');
template = 'echo(`' + template + '`);';
而後,將template封裝在一個函數裏面返回,就能夠了。
var script =
`(function parse(data){
var output = "";
function echo(html){
output += html;
}
${ template }
return output;
})`;
return script;
將上面的內容拼裝成一個模板編譯函數compile。
function compile(template){
var evalExpr = /<%=(.+?)%>/g;
var expr = /<%([\s\S]+?)%>/g;
template = template
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
.replace(expr, '`); \n $1 \n echo(`');
template = 'echo(`' + template + '`);';
var script =
`(function parse(data){
var output = "";
function echo(html){
output += html;
}
${ template }
return output;
})`;
return script;
}
compile函數的用法以下。
var parse = eval(compile(template));
div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>
4.12 標籤模板
模板字符串的功能,不只僅是上面這些。它能夠緊跟在一個函數名後面,該函數將被調用來處理這個模板字符串。這被稱爲「標籤模板」功能(tagged
template)。
alert`123`
// 等同於
alert(123)
標籤模板其實不是模板,而是函數調用的一種特殊形式。「標籤」指的就是函數,緊跟在後面的模板字符串就是它的參數。
可是,若是模板字符裏面有變量,就不是簡單的調用了,而是會將模板字符串先處理成多個參數,再調用函數。
var a = 5;
var b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同於
tag(['Hello ', ' world ', ''], 15, 50);
上面代碼中,模板字符串前面有一個標識名tag,它是一個函數。整個表達式的返回值,就是tag函數處理模板字符串後的返回值。
函數tag依次會接收到多個參數。
function tag(stringArr, value1, value2){
// ...
}
// 等同於
function tag(stringArr, ...values){
// ...
}
tag函數的第一個參數是一個數組,該數組的成員是模板字符串中那些沒有變量替換的部分,也就是說,變量替換隻發生在數組的第一個成員與第二個
成員之間、第二個成員與第三個成員之間,以此類推。
tag函數的其餘參數,都是模板字符串各個變量被替換後的值。因爲本例中,模板字符串含有兩個變量,所以tag會接受到value1和value2兩個參數。
tag函數全部參數的實際值以下。
第一個參數:['Hello ', ' world ', '']
第二個參數: 15
第三個參數:50
也就是說,tag函數實際上如下面的形式調用。
tag(['Hello ', ' world ', ''], 15, 50)
咱們能夠按照須要編寫tag函數的代碼。下面是tag函數的一種寫法,以及運行結果。
var a = 5;
var b = 10;
function tag(s, v1, v2) {
console.log(s[0]);
console.log(s[1]);
console.log(s[2]);
console.log(v1);
console.log(v2);
return "OK";
}
tag`Hello ${ a + b } world ${ a * b}`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"
下面是一個更復雜的例子。
var total = 30;
var msg = passthru`The total is ${total} (${total*1.05} with tax)`;
function passthru(literals) {
var result = '';
var i = 0;
while (i < literals.length) {
result += literals[i++];
if (i < arguments.length) {
result += arguments[i];
}
}
return result;
}
msg // "The total is 30 (31.5 with tax)"
上面這個例子展現了,如何將各個參數按照原來的位置拼合回去。
passthru函數採用rest參數的寫法以下。
function passthru(literals, ...values) {
var output = "";
for (var index = 0; index < values.length; index++) {
output += literals[index] + values[index];
}
output += literals[index]
return output;
}
「標籤模板」的一個重要應用,就是過濾HTML字符串,防止用戶輸入惡意內容。
var message =
SaferHTML`<p>${sender} has sent you a message.</p>`;
function SaferHTML(templateData) {
var s = templateData[0];
for (var i = 1; i < arguments.length; i++) {
var arg = String(arguments[i]);
// Escape special characters in the substitution.
s += arg.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
上面代碼中,sender變量每每是用戶提供的,通過SaferHTML函數處理,裏面的特殊字符都會被轉義。
var sender = '<script>alert("abc")</script>'; // 惡意代碼
var message = SaferHTML`<p>${sender} has sent you a message.</p>`;
message
// <p><script>alert("abc")</script> has sent you a message.</p>
標籤模板的另外一個應用,就是多語言轉換(國際化處理)。
i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
// "歡迎訪問xxx,您是第xxxx位訪問者!"
模板字符串自己並不能取代Mustache之類的模板庫,由於沒有條件判斷和循環處理功能,可是經過標籤函數,你能夠本身添加這些功能。
// 下面的hashTemplate函數
// 是一個自定義的模板處理函數
var libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
#end
</ul>
`;
除此以外,你甚至能夠使用標籤模板,在JavaScript語言之中嵌入其餘語言。
jsx`
<div>
<input
ref='input'
onChange='${this.handleChange}'
defaultValue='${this.state.value}' />
${this.state.value}
</div>
`
上面的代碼經過jsx函數,將一個DOM字符串轉爲React對象。你能夠在Github找到jsx函數的具體實現。
下面則是一個假想的例子,經過java函數,在JavaScript代碼之中運行Java代碼。
java`
class HelloWorldApp {
public static void main(String[] args) {
System.out.println(「Hello World!」); // Display the string.
}
}
`
HelloWorldApp.main();
模板處理函數的第一個參數(模板字符串數組),還有一個raw屬性。
tag`First line\nSecond line`
function tag(strings) {
console.log(strings.raw[0]);
// "First line\\nSecond line"
}
上面代碼中,tag函數的第一個參數strings,有一個raw屬性,也指向一個數組。該數組的成員與strings數組徹底一致。好比,strings數組
是["First line\nSecond line"],那麼strings.raw數組就是["First line\\nSecond line"]。二者惟一的區別,就是字符串裏面的斜槓都被轉義
了。好比,strings.raw數組會將\n視爲\和n兩個字符,而不是換行符。這是爲了方便取得轉義以前的原始模板而設計的。
4.13 String.raw()
ES6還爲原生的String對象,提供了一個raw方法。
String.raw方法,每每用來充當模板字符串的處理函數,返回一個斜槓都被轉義(即斜槓前面再加一個斜槓)的字符串,對應於替換變量後的模板字
符串。
String.raw`Hi\n${2+3}!`;
// "Hi\\n5!"
String.raw`Hi\u000A!`;
// 'Hi\\u000A!'
若是原字符串的斜槓已經轉義,那麼String.raw不會作任何處理。
String.raw`Hi\\n`
// "Hi\\n"
String.raw的代碼基本以下。
String.raw = function (strings, ...values) {
var output = "";
for (var index = 0; index < values.length; index++) {
output += strings.raw[index] + values[index];
}
output += strings.raw[index]
return output;
}
String.raw方法能夠做爲處理模板字符串的基本方法,它會將全部變量替換,並且對斜槓進行轉義,方便下一步做爲字符串來使用。
String.raw方法也能夠做爲正常的函數使用。這時,它的第一個參數,應該是一個具備raw屬性的對象,且raw屬性的值應該是一個數組。
String.raw({ raw: 'test' }, 0, 1, 2);
// 't0e1s2t'
// 等同於
String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
5 正則的擴展
5.1 RegExp構造函數
在ES5中,RegExp構造函數的參數有兩種狀況。
第一種狀況是,參數是字符串,這時第二個參數表示正則表達式的修飾符(flag)。
var regex = new RegExp('xyz', 'i');
// 等價於
var regex = /xyz/i;
第二種狀況是,參數是一個正則表示式,這時會返回一個原有正則表達式的拷貝。
var regex = new RegExp(/xyz/i);
// 等價於
var regex = /xyz/i;
可是,ES5不容許此時使用第二個參數,添加修飾符,不然會報錯。
var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
ES6改變了這種行爲。若是RegExp構造函數第一個參數是一個正則對象,那麼能夠使用第二個參數指定修飾符。並且,返回的正則表達式會忽略原有
的正則表達式的修飾符,只使用新指定的修飾符。
new RegExp(/abc/ig, 'i').flags
// "i"
上面代碼中,原有正則對象的修飾符是ig,它會被第二個參數i覆蓋。
5.2 字符串的正則方法
字符串對象共有4個方法,能夠使用正則表達式:match()、replace()、search()和split()。
ES6將這4個方法,在語言內部所有調用RegExp的實例方法,從而作到全部與正則相關的方法,全都定義在RegExp對象上。
String.prototype.match 調用 RegExp.prototype[Symbol.match]
String.prototype.replace 調用 RegExp.prototype[Symbol.replace]
String.prototype.search 調用 RegExp.prototype[Symbol.search]
String.prototype.split 調用 RegExp.prototype[Symbol.split]
5.3 u修飾符
ES6對正則表達式添加了u修飾符,含義爲「Unicode模式」,用來正確處理大於\uFFFF的Unicode字符。也就是說,會正確處理四個字節的UTF-16編
碼。
/^\uD83D/u.test('\uD83D\uDC2A')
// false
/^\uD83D/.test('\uD83D\uDC2A')
// true
上面代碼中,\uD83D\uDC2A是一個四個字節的UTF-16編碼,表明一個字符。可是,ES5不支持四個字節的UTF-16編碼,會將其識別爲兩個字符,致使
第二行代碼結果爲true。加了u修飾符之後,ES6就會識別其爲一個字符,因此第一行代碼結果爲false。
一旦加上u修飾符號,就會修改下面這些正則表達式的行爲。
(1)點字符
點(.)字符在正則表達式中,含義是除了換行符之外的任意單個字符。對於碼點大於0xFFFF的Unicode字符,點字符不能識別,必須加上u修飾符。
var s = '';
/^.$/.test(s) // false
/^.$/u.test(s) // true
上面代碼表示,若是不添加u修飾符,正則表達式就會認爲字符串爲兩個字符,從而匹配失敗。
(2)Unicode字符表示法
ES6新增了使用大括號表示Unicode字符,這種表示法在正則表達式中必須加上u修飾符,才能識別。
/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('') // true
上面代碼表示,若是不加u修飾符,正則表達式沒法識別\u{61}這種表示法,只會認爲這匹配61個連續的u。
(3)量詞
使用u修飾符後,全部量詞都會正確識別碼點大於0xFFFF的Unicode字符。
/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/{2}/.test('') // false
/{2}/u.test('') // true
另外,只有在使用u修飾符的狀況下,Unicode表達式當中的大括號纔會被正確解讀,不然會被解讀爲量詞。
/^\u{3}$/.test('uuu') // true
上面代碼中,因爲正則表達式沒有u修飾符,因此大括號被解讀爲量詞。加上u修飾符,就會被解讀爲Unicode表達式。
(4)預約義模式
u修飾符也影響到預約義模式,可否正確識別碼點大於0xFFFF的Unicode字符。
/^\S$/.test('') // false
/^\S$/u.test('') // true
上面代碼的\S是預約義模式,匹配全部不是空格的字符。只有加了u修飾符,它才能正確匹配碼點大於0xFFFF的Unicode字符。
利用這一點,能夠寫出一個正確返回字符串長度的函數。
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
var s = '';
s.length // 4
codePointLength(s) // 2
(5)i修飾符
有些Unicode字符的編碼不一樣,可是字型很相近,好比,\u004B與\u212A都是大寫的K。
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
上面代碼中,不加u修飾符,就沒法識別非規範的K字符。
5.4 y修飾符
除了u修飾符,ES6還爲正則表達式添加了y修飾符,叫作「粘連」(sticky)修飾符。
y修飾符的做用與g修飾符相似,也是全局匹配,後一次匹配都從上一次匹配成功的下一個位置開始。不一樣之處在於,g修飾符只要剩餘位置中存在匹配
就可,而y修飾符確保匹配必須從剩餘的第一個位置開始,這也就是「粘連」的涵義。
var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
上面代碼有兩個正則表達式,一個使用g修飾符,另外一個使用y修飾符。這兩個正則表達式各執行了兩次,第一次執行的時候,二者行爲相同,剩餘字
符串都是_aa_a。因爲g修飾沒有位置要求,因此第二次執行會返回結果,而y修飾符要求匹配必須從頭部開始,因此返回null。
若是改一下正則表達式,保證每次都能頭部匹配,y修飾符就會返回結果了。
var s = 'aaa_aa_a';
var r = /a+_/y;
r.exec(s) // ["aaa_"]
r.exec(s) // ["aa_"]
上面代碼每次匹配,都是從剩餘字符串的頭部開始。
使用lastIndex屬性,能夠更好地說明y修飾符。
const REGEX = /a/g;
// 指定從2號位置(y)開始匹配
REGEX.lastIndex = 2;
// 匹配成功
const match = REGEX.exec('xaya');
// 在3號位置匹配成功
match.index // 3
// 下一次匹配從4號位開始
REGEX.lastIndex // 4
// 4號位開始匹配失敗
REGEX.exec('xaxa') // null
上面代碼中,lastIndex屬性指定每次搜索的開始位置,g修飾符從這個位置開始向後搜索,直到發現匹配爲止。
y修飾符一樣遵照lastIndex屬性,可是要求必須在lastIndex指定的位置發現匹配。
const REGEX = /a/y;
// 指定從2號位置開始匹配
REGEX.lastIndex = 2;
// 不是粘連,匹配失敗
REGEX.exec('xaya') // null
// 指定從3號位置開始匹配
REGEX.lastIndex = 3;
// 3號位置是粘連,匹配成功
const match = REGEX.exec('xaxa');
match.index // 3
REGEX.lastIndex // 4
進一步說,y修飾符號隱含了頭部匹配的標誌^。
/b/y.exec('aba')
// null
上面代碼因爲不能保證頭部匹配,因此返回null。y修飾符的設計本意,就是讓頭部匹配的標誌^在全局匹配中都有效。
在split方法中使用y修飾符,原字符串必須以分隔符開頭。這也意味着,只要匹配成功,數組的第一個成員確定是空字符串。
// 沒有找到匹配
'x##'.split(/#/y)
// [ 'x##' ]
// 找到兩個匹配
'##x'.split(/#/y)
// [ '', '', 'x' ]
後續的分隔符只有緊跟前面的分隔符,纔會被識別。
'#x#'.split(/#/y)
// [ '', 'x#' ]
'##'.split(/#/y)
// [ '', '', '' ]
下面是字符串對象的replace方法的例子。
const REGEX = /a/gy;
'aaxa'.replace(REGEX, '-') // '--xa'
上面代碼中,最後一個a由於不是出現下一次匹配的頭部,因此不會被替換。
單單一個y修飾符對match方法,只能返回第一個匹配,必須與g修飾符聯用,才能返回全部匹配。
'a1a2a3'.match(/a\d/y) // ["a1"]
'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]
y修飾符的一個應用,是從字符串提取token(詞元),y修飾符確保了匹配之間不會有漏掉的字符。
const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
const TOKEN_G = /\s*(\+|[0-9]+)\s*/g;
tokenize(TOKEN_Y, '3 + 4')
// [ '3', '+', '4' ]
tokenize(TOKEN_G, '3 + 4')
// [ '3', '+', '4' ]
function tokenize(TOKEN_REGEX, str) {
let result = [];
let match;
while (match = TOKEN_REGEX.exec(str)) {
result.push(match[1]);
}
return result;
}
上面代碼中,若是字符串裏面沒有非法字符,y修飾符與g修飾符的提取結果是同樣的。可是,一旦出現非法字符,二者的行爲就不同了。
tokenize(TOKEN_Y, '3x + 4')
// [ '3' ]
tokenize(TOKEN_G, '3x + 4')
// [ '3', '+', '4' ]
上面代碼中,g修飾符會忽略非法字符,而y修飾符不會,這樣就很容易發現錯誤。
5.5 sticky屬性
與y修飾符相匹配,ES6的正則對象多了sticky屬性,表示是否設置了y修飾符。
var r = /hello\d/y;
r.sticky // true
5.6 flags屬性
ES6爲正則表達式新增了flags屬性,會返回正則表達式的修飾符。
// ES5的source屬性
// 返回正則表達式的正文
/abc/ig.source
// "abc"
// ES6的flags屬性
// 返回正則表達式的修飾符
/abc/ig.flags
// 'gi'
5.7 RegExp.escape()
字符串必須轉義,才能做爲正則模式。
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}
let str = '/path/to/resource.html?search=query';
escapeRegExp(str)
// "\/path\/to\/resource\.html\?search=query"
上面代碼中,str是一個正常字符串,必須使用反斜槓對其中的特殊字符轉義,才能用來做爲一個正則匹配的模式。
已經有提議將這個需求標準化,做爲RegExp對象的靜態方法RegExp.escape(),放入ES7。2015年7月31日,TC39認爲,這個方法有安全風險,又不
願這個方法變得過於複雜,沒有贊成將其列入ES7,但這不失爲一個真實的需求。
RegExp.escape('The Quick Brown Fox');
// "The Quick Brown Fox"
RegExp.escape('Buy it. use it. break it. fix it.');
// "Buy it\. use it\. break it\. fix it\."
RegExp.escape('(*.*)');
// "\(\*\.\*\)"
字符串轉義之後,能夠使用RegExp構造函數生成正則模式。
var str = 'hello. how are you?';
var regex = new RegExp(RegExp.escape(str), 'g');
var regex = new RegExp(RegExp.escape(str), 'g');
assert.equal(String(regex), '/hello\. how are you\?/g');
目前,該方法能夠用上文的escapeRegExp函數或者墊片模塊regexp.escape實現。
var escape = require('regexp.escape');
escape('hi. how are you?');
// "hi\\. how are you\\?"
5.8 後行斷言
JavaScript語言的正則表達式,只支持先行斷言(lookahead)和先行否認斷言(negative lookahead),不支持後行斷言(lookbehind)和後行否認
斷言(negative lookbehind)。
目前,有一個提案,在ES7加入後行斷言。V8引擎4.9版已經支持,Chrome瀏覽器49版打開」experimental JavaScript features「開關(地址欄鍵
入about:flags),就能夠使用這項功能。
」先行斷言「指的是,x只有在y前面才匹配,必須寫成/x(?=y)/。好比,只匹配百分號以前的數字,要寫成/\d+(?=%)/。」先行否認斷言「指的是,x只有
不在y前面才匹配,必須寫成/x(?!y)/。好比,只匹配不在百分號以前的數字,要寫成/\d+(?!%)/。
/\d+(?=%)/.exec('100% of US presidents have been male') // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them') // ["44"]
上面兩個字符串,若是互換正則表達式,就會匹配失敗。另外,還能夠看到,」先行斷言「括號之中的部分((?=%)),是不計入返回結果的。
"後行斷言"正好與"先行斷言"相反,x只有在y後面才匹配,必須寫成/(?<=y)x/。好比,只匹配美圓符號以後的數字,要寫成/(?<=\$)\d+/。」後行否認
斷言「則與」先行否認斷言「相反,x只有不在y後面才匹配,必須寫成/(?<!y)x/。好比,只匹配不在美圓符號後面的數字,要寫成/(?<!\$)\d+/。
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
上面的例子中,"後行斷言"的括號之中的部分((?<=\$)),也是不計入返回結果。
"後行斷言"的實現,須要先匹配/(?<=y)x/的x,而後再回到左邊,匹配y的部分。這種"先右後左"的執行順序,與全部其餘正則操做相反,致使了一些
不符合預期的行爲。
首先,」後行斷言「的組匹配,與正常狀況下結果是不同的。
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
上面代碼中,須要捕捉兩個組匹配。沒有"後行斷言"時,第一個括號是貪婪模式,第二個括號只能捕獲一個字符,因此結果是105和3。而"後行斷
言"時,因爲執行順序是從右到左,第二個括號是貪婪模式,第一個括號只能捕獲一個字符,因此結果是1和053。
其次,"後行斷言"的反斜槓引用,也與一般的順序相反,必須放在對應的那個括號以前。
/(?<=(o)d\1)r/.exec('hodor') // null
/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
上面代碼中,若是後行斷言的反斜槓引用(\1)放在括號的後面,就不會獲得匹配結果,必須放在前面才能夠。
6 數值的擴展
6.1 二進制和八進制表示法
ES6提供了二進制和八進制數值的新的寫法,分別用前綴0b(或0B)和0o(或0O)表示。
0b111110111 === 503 // true
0o767 === 503 // true
從ES5開始,在嚴格模式之中,八進制就再也不容許使用前綴0表示,ES6進一步明確,要使用前綴0o表示。
// 非嚴格模式
(function(){
console.log(0o11 === 011);
})() // true
// 嚴格模式
(function(){
'use strict';
console.log(0o11 === 011);
})() // Uncaught SyntaxError: Octal literals are not allowed in strict mode.
若是要將0b和0o前綴的字符串數值轉爲十進制,要使用Number方法。
Number('0b111') // 7
Number('0o10') // 8
6.2 Number.isFinite(), Number.isNaN()
ES6在Number對象上,新提供了Number.isFinite()和Number.isNaN()兩個方法。
Number.isFinite()用來檢查一個數值是否爲有限的(finite)。
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
ES5能夠經過下面的代碼,部署Number.isFinite方法。
(function (global) {
var global_isFinite = global.isFinite;
Object.defineProperty(Number, 'isFinite', {
value: function isFinite(value) {
return typeof value === 'number' && global_isFinite(value);
},
configurable: true,
enumerable: false,
writable: true
});
})(this);
Number.isNaN()用來檢查一個值是否爲NaN。
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true'/0) // true
Number.isNaN('true'/'true') // true
ES5經過下面的代碼,部署Number.isNaN()。
(function (global) {
var global_isNaN = global.isNaN;
Object.defineProperty(Number, 'isNaN', {
value: function isNaN(value) {
return typeof value === 'number' && global_isNaN(value);
},
configurable: true,
enumerable: false,
writable: true
});
})(this);
它們與傳統的全局方法isFinite()和isNaN()的區別在於,傳統方法先調用Number()將非數值的值轉爲數值,再進行判斷,而這兩個新方法只對數值有
效,非數值一概返回false。
isFinite(25) // true
isFinite("25") // true
Number.isFinite(25) // true
Number.isFinite("25") // false
isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
6.3 Number.parseInt(), Number.parseFloat()
ES6將全局方法parseInt()和parseFloat(),移植到Number對象上面,行爲徹底保持不變。
// ES5的寫法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45
// ES6的寫法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
這樣作的目的,是逐步減小全局性方法,使得語言逐步模塊化。
Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
6.4 Number.isInteger()
Number.isInteger()用來判斷一個值是否爲整數。須要注意的是,在JavaScript內部,整數和浮點數是一樣的儲存方法,因此3和3.0被視爲同一個值。
Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false
ES5能夠經過下面的代碼,部署Number.isInteger()。
(function (global) {
var floor = Math.floor,
isFinite = global.isFinite;
Object.defineProperty(Number, 'isInteger', {
value: function isInteger(value) {
return typeof value === 'number' && isFinite(value) &&
value > -9007199254740992 && value < 9007199254740992 &&
floor(value) === value;
},
configurable: true,
enumerable: false,
writable: true
});
})(this);
6.5 Number.EPSILON
ES6在Number對象上面,新增一個極小的常量Number.EPSILON。
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// '0.00000000000000022204'
引入一個這麼小的量的目的,在於爲浮點數計算,設置一個偏差範圍。咱們知道浮點數計算是不精確的。
0.1 + 0.2
// 0.30000000000000004
0.1 + 0.2 - 0.3
// 5.551115123125783e-17
5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'
可是若是這個偏差可以小於Number.EPSILON,咱們就能夠認爲獲得了正確結果。
5.551115123125783e-17 < Number.EPSILON
// true
所以,Number.EPSILON的實質是一個能夠接受的偏差範圍。
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON;
}
withinErrorMargin(0.1 + 0.2, 0.3)
// true
withinErrorMargin(0.2 + 0.2, 0.3)
// false
上面的代碼爲浮點數運算,部署了一個偏差檢查函數。
6.6 安全整數和Number.isSafeInteger()
JavaScript可以準確表示的整數範圍在-2^53到2^53之間(不含兩個端點),超過這個範圍,沒法精確表示這個值。
Math.pow(2, 53) // 9007199254740992
9007199254740992 // 9007199254740992
9007199254740993 // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1
// true
上面代碼中,超出2的53次方以後,一個數就不精確了。
ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER這兩個常量,用來表示這個範圍的上下限。
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true
上面代碼中,能夠看到JavaScript可以精確表示的極限。
Number.isSafeInteger()則是用來判斷一個整數是否落在這個範圍以內。
Number.isSafeInteger('a') // false
Number.isSafeInteger(null) // false
Number.isSafeInteger(NaN) // false
Number.isSafeInteger(Infinity) // false
Number.isSafeInteger(-Infinity) // false
Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
這個函數的實現很簡單,就是跟安全整數的兩個邊界值比較一下。
Number.isSafeInteger = function (n) {
return (typeof n === 'number' &&
Math.round(n) === n &&
Number.MIN_SAFE_INTEGER <= n &&
n <= Number.MAX_SAFE_INTEGER);
}
實際使用這個函數時,須要注意。驗證運算結果是否落在安全整數的範圍內,不要只驗證運算結果,而要同時驗證參與運算的每一個值。
Number.isSafeInteger(9007199254740993)
// false
Number.isSafeInteger(990)
// true
Number.isSafeInteger(9007199254740993 - 990)
// true
9007199254740993 - 990
// 返回結果 9007199254740002
// 正確答案應該是 9007199254740003
上面代碼中,9007199254740993不是一個安全整數,可是Number.isSafeInteger會返回結果,顯示計算結果是安全的。這是由於,這個數超出了精度
範圍,致使在計算機內部,以9007199254740992的形式儲存。
9007199254740993 === 9007199254740992
// true
因此,若是隻驗證運算結果是否爲安全整數,極可能獲得錯誤結果。下面的函數能夠同時驗證兩個運算數和運算結果。
function trusty (left, right, result) {
if (
Number.isSafeInteger(left) &&
Number.isSafeInteger(right) &&
Number.isSafeInteger(result)
) {
return result;
}
throw new RangeError('Operation cannot be trusted!');
}
trusty(9007199254740993, 990, 9007199254740993 - 990)
// RangeError: Operation cannot be trusted!
trusty(1, 2, 3)
// 3
6.7 Math對象的擴展
ES6在Math對象上新增了17個與數學相關的方法。全部這些方法都是靜態方法,只能在Math對象上調用。
Math.trunc()
Math.trunc方法用於去除一個數的小數部分,返回整數部分。
Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0
對於非數值,Math.trunc內部使用Number方法將其先轉爲數值。
Math.trunc('123.456')
// 123
對於空值和沒法截取整數的值,返回NaN。
Math.trunc(NaN); // NaN
Math.trunc('foo'); // NaN
Math.trunc(); // NaN
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.trunc = Math.trunc || function(x) {
return x < 0 ? Math.ceil(x) : Math.floor(x);
};
6.7.1 Math.sign()
Math.sign方法用來判斷一個數究竟是正數、負數、仍是零。
它會返回五種值。
參數爲正數,返回+1;
參數爲負數,返回-1;
參數爲0,返回0;
參數爲-0,返回-0;
其餘值,返回NaN。
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN
Math.sign('foo'); // NaN
Math.sign(); // NaN
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.sign = Math.sign || function(x) {
x = +x; // convert to a number
if (x === 0 || isNaN(x)) {
return x;
}
return x > 0 ? 1 : -1;
};
6.7.2 Math.cbrt()
Math.cbrt方法用於計算一個數的立方根。
Math.cbrt(-1) // -1
Math.cbrt(0) // 0
Math.cbrt(1) // 1
Math.cbrt(2) // 1.2599210498948734
對於非數值,Math.cbrt方法內部也是先使用Number方法將其轉爲數值。
Math.cbrt('8') // 2
Math.cbrt('hello') // NaN
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.cbrt = Math.cbrt || function(x) {
var y = Math.pow(Math.abs(x), 1/3);
return x < 0 ? -y : y;
};
6.7.3 Math.clz32()
JavaScript的整數使用32位二進制形式表示,Math.clz32方法返回一個數的32位無符號整數形式有多少個前導0。
Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2
上面代碼中,0的二進制形式全爲0,因此有32個前導0;1的二進制形式是0b1,只佔1位,因此32位之中有31個前導0;1000的二進制形式
是0b1111101000,一共有10位,因此32位之中有22個前導0。
clz32這個函數名就來自」count leading zero bits in 32-bit binary representations of a number「(計算32位整數的前導0)的縮寫。
左移運算符(<<)與Math.clz32方法直接相關。
Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1 << 1) // 30
Math.clz32(1 << 2) // 29
Math.clz32(1 << 29) // 2
對於小數,Math.clz32方法只考慮整數部分。
Math.clz32(3.2) // 30
Math.clz32(3.9) // 30
對於空值或其餘類型的值,Math.clz32方法會將它們先轉爲數值,而後再計算。
Math.clz32() // 32
Math.clz32(NaN) // 32
Math.clz32(Infinity) // 32
Math.clz32(null) // 32
Math.clz32('foo') // 32
Math.clz32([]) // 32
Math.clz32({}) // 32
Math.clz32(true) // 31
6.7.4 Math.imul()
Math.imul方法返回兩個數以32位帶符號整數形式相乘的結果,返回的也是一個32位的帶符號整數。
Math.imul(2, 4) // 8
Math.imul(-1, 8) // -8
Math.imul(-2, -2) // 4
若是隻考慮最後32位,大多數狀況下,Math.imul(a, b)與a * b的結果是相同的,即該方法等同於(a * b)|0的效果(超過32位的部分溢出)。之所
以須要部署這個方法,是由於JavaScript有精度限制,超過2的53次方的值沒法精確表示。這就是說,對於那些很大的數的乘法,低位數值每每都是不
精確的,Math.imul方法能夠返回正確的低位數值。
(0x7fffffff * 0x7fffffff)|0 // 0
上面這個乘法算式,返回結果爲0。可是因爲這兩個二進制數的最低位都是1,因此這個結果確定是不正確的,由於根據二進制乘法,計算結果的二進
制最低位應該也是1。這個錯誤就是由於它們的乘積超過了2的53次方,JavaScript沒法保存額外的精度,就把低位的值都變成了0。Math.imul方法可
以返回正確的值1。
Math.imul(0x7fffffff, 0x7fffffff) // 1
6.7.5 Math.fround()
Math.fround方法返回一個數的單精度浮點數形式。
Math.fround(0) // 0
Math.fround(1) // 1
Math.fround(1.337) // 1.3370000123977661
Math.fround(1.5) // 1.5
Math.fround(NaN) // NaN
對於整數來講,Math.fround方法返回結果不會有任何不一樣,區別主要是那些沒法用64個二進制位精確表示的小數。這時,Math.fround方法會返回最
接近這個小數的單精度浮點數。
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.fround = Math.fround || function(x) {
return new Float32Array([x])[0];
};
6.7.6 Math.hypot()
Math.hypot方法返回全部參數的平方和的平方根。
Math.hypot(3, 4); // 5
Math.hypot(3, 4, 5); // 7.0710678118654755
Math.hypot(); // 0
Math.hypot(NaN); // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5'); // 7.0710678118654755
Math.hypot(-3); // 3
上面代碼中,3的平方加上4的平方,等於5的平方。
若是參數不是數值,Math.hypot方法會將其轉爲數值。只要有一個參數沒法轉爲數值,就會返回NaN。
6.7.7 對數方法
ES6新增了4個對數相關方法。
(1) Math.expm1()
Math.expm1(x)返回e
x
- 1,即Math.exp(x) - 1。
Math.expm1(-1) // -0.6321205588285577
Math.expm1(0) // 0
Math.expm1(1) // 1.718281828459045
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.expm1 = Math.expm1 || function(x) {
return Math.exp(x) - 1;
};
(2)Math.log1p()
Math.log1p(x)方法返回1 + x的天然對數,即Math.log(1 + x)。若是x小於-1,返回NaN。
Math.log1p(1) // 0.6931471805599453
Math.log1p(0) // 0
Math.log1p(-1) // -Infinity
Math.log1p(-2) // NaN
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.log1p = Math.log1p || function(x) {
return Math.log(1 + x);
};
(3)Math.log10()
Math.log10(x)返回以10爲底的x的對數。若是x小於0,則返回NaN。
Math.log10(2) // 0.3010299956639812
Math.log10(1) // 0
Math.log10(0) // -Infinity
Math.log10(-2) // NaN
Math.log10(100000) // 5
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.log10 = Math.log10 || function(x) {
return Math.log(x) / Math.LN10;
};
(4)Math.log2()
Math.log2(x)返回以2爲底的x的對數。若是x小於0,則返回NaN。
Math.log2(3) // 1.584962500721156
Math.log2(2) // 1
Math.log2(1) // 0
Math.log2(0) // -Infinity
Math.log2(-2) // NaN
Math.log2(1024) // 10
Math.log2(1 << 29) // 29
對於沒有部署這個方法的環境,能夠用下面的代碼模擬。
Math.log2 = Math.log2 || function(x) {
return Math.log(x) / Math.LN2;
};
6.7.8 三角函數方法
ES6新增了6個三角函數方法。
Math.sinh(x) 返回x的雙曲正弦(hyperbolic sine)
Math.cosh(x) 返回x的雙曲餘弦(hyperbolic cosine)
Math.tanh(x) 返回x的雙曲正切(hyperbolic tangent)
Math.asinh(x) 返回x的反雙曲正弦(inverse hyperbolic sine)
Math.acosh(x) 返回x的反雙曲餘弦(inverse hyperbolic cosine)
Math.atanh(x) 返回x的反雙曲正切(inverse hyperbolic tangent)
6.8 指數運算符
ES7新增了一個指數運算符(**),目前Babel轉碼器已經支持。
2 ** 2 // 4
2 ** 3 // 8
指數運算符能夠與等號結合,造成一個新的賦值運算符(**=)。
let a = 2;
a **= 2;
// 等同於 a = a * a;
let b = 3;
b **= 3;
// 等同於 b = b * b * b;
7 數組的擴展
7.1 Array.from()
Array.from方法用於將兩類對象轉爲真正的數組:相似數組的對象(array-like object)和可遍歷(iterable)的對象(包括ES6新增的數據結構Set和
Map)。
下面是一個相似數組的對象,Array.from將它轉爲真正的數組。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的寫法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的寫法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
實際應用中,常見的相似數組的對象是DOM操做返回的NodeList集合,以及函數內部的arguments對象。Array.from均可以將它們轉爲真正的數組。
// NodeList對象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
console.log(p);
});
// arguments對象
function foo() {
var args = Array.from(arguments);
// ...
}
上面代碼中,querySelectorAll方法返回的是一個相似數組的對象,只有將這個對象轉爲真正的數組,才能使用forEach方法。
只要是部署了Iterator接口的數據結構,Array.from都能將其轉爲數組。
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
上面代碼中,字符串和Set結構都具備Iterator接口,所以能夠被Array.from轉爲真正的數組。
若是參數是一個真正的數組,Array.from會返回一個如出一轍的新數組。
Array.from([1, 2, 3])
// [1, 2, 3]
值得提醒的是,擴展運算符(...)也能夠將某些數據結構轉爲數組。
// arguments對象
function foo() {
var args = [...arguments];
}
// NodeList對象
[...document.querySelectorAll('div')]
擴展運算符背後調用的是遍歷器接口(Symbol.iterator),若是一個對象沒有部署這個接口,就沒法轉換。Array.from方法則是還支持相似數組的對
象。所謂相似數組的對象,本質特徵只有一點,即必須有length屬性。所以,任何有length屬性的對象,均可以經過Array.from方法轉爲數組,而此
時擴展運算符就沒法轉換。
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
上面代碼中,Array.from返回了一個具備三個成員的數組,每一個位置的值都是undefined。擴展運算符轉換不了這個對象。
對於尚未部署該方法的瀏覽器,能夠用Array.prototype.slice方法替代。
const toArray = (() =>
Array.from ? Array.from : obj => [].slice.call(obj)
)();
Array.from還能夠接受第二個參數,做用相似於數組的map方法,用來對每一個元素進行處理,將處理後的值放入返回的數組。
Array.from(arrayLike, x => x * x);
// 等同於
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
下面的例子是取出一組DOM節點的文本內容。
let spans = document.querySelectorAll('span.name');
// map()
let names1 = Array.prototype.map.call(spans, s => s.textContent);
// Array.from()
let names2 = Array.from(spans, s => s.textContent)
下面的例子將數組中布爾值爲false的成員轉爲0。
Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]
另外一個例子是返回各類數據的類型。
function typesOf () {
return Array.from(arguments, value => typeof value)
}
typesOf(null, [], NaN)
// ['object', 'object', 'number']
若是map函數裏面用到了this關鍵字,還能夠傳入Array.from的第三個參數,用來綁定this。
Array.from()能夠將各類值轉爲真正的數組,而且還提供map功能。這實際上意味着,只要有一個原始的數據結構,你就能夠先對它的值進行處理,然
後轉成規範的數組結構,進而就能夠使用數量衆多的數組方法。
Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']
上面代碼中,Array.from的第一個參數指定了第二個參數運行的次數。這種特性可讓該方法的用法變得很是靈活。
Array.from()的另外一個應用是,將字符串轉爲數組,而後返回字符串的長度。由於它能正確處理各類Unicode字符,能夠避免JavaScript將大
於\uFFFF的Unicode字符,算做兩個字符的bug。
function countSymbols(string) {
return Array.from(string).length;
}
7.2 Array.of()
Array.of方法用於將一組值,轉換爲數組。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
這個方法的主要目的,是彌補數組構造函數Array()的不足。由於參數個數的不一樣,會致使Array()的行爲有差別。
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
上面代碼中,Array方法沒有參數、一個參數、三個參數時,返回結果都不同。只有當參數個數很多於2個時,Array()纔會返回由參數組成的新數
組。參數個數只有一個時,其實是指定數組的長度。
Array.of基本上能夠用來替代Array()或new Array(),而且不存在因爲參數不一樣而致使的重載。它的行爲很是統一。
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
Array.of老是返回參數值組成的數組。若是沒有參數,就返回一個空數組。
Array.of方法能夠用下面的代碼模擬實現。
function ArrayOf(){
return [].slice.call(arguments);
}
7.3 數組實例的copyWithin()
數組實例的copyWithin方法,在當前數組內部,將指定位置的成員複製到其餘位置(會覆蓋原有成員),而後返回當前數組。也就是說,使用這個方
法,會修改當前數組。
Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三個參數。
target(必需):從該位置開始替換數據。
start(可選):從該位置開始讀取數據,默認爲0。若是爲負值,表示倒數。
end(可選):到該位置前中止讀取數據,默認等於數組長度。若是爲負值,表示倒數。
這三個參數都應該是數值,若是不是,會自動轉爲數值。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
上面代碼表示將從3號位直到數組結束的成員(4和5),複製到從0號位開始的位置,結果覆蓋了原來的1和2。
下面是更多例子。
// 將3號位複製到0號位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]
// -2至關於3號位,-1至關於4號位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]
// 將3號位複製到0號位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}
// 將2號位到數組結束,複製到0號位
var i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]
// 對於沒有部署TypedArray的copyWithin方法的平臺
// 須要採用下面的寫法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]
7.4 數組實例的find()和findIndex()
數組實例的find方法,用於找出第一個符合條件的數組成員。它的參數是一個回調函數,全部數組成員依次執行該回調函數,直到找出第一個返回值
爲true的成員,而後返回該成員。若是沒有符合條件的成員,則返回undefined。
[1, 4, -5, 10].find((n) => n < 0)
// -5
上面代碼找出數組中第一個小於0的成員。
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
上面代碼中,find方法的回調函數能夠接受三個參數,依次爲當前的值、當前的位置和原數組。
數組實例的findIndex方法的用法與find方法很是相似,返回第一個符合條件的數組成員的位置,若是全部成員都不符合條件,則返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
這兩個方法均可以接受第二個參數,用來綁定回調函數的this對象。
另外,這兩個方法均可以發現NaN,彌補了數組的IndexOf方法的不足。
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
上面代碼中,indexOf方法沒法識別數組的NaN成員,可是findIndex方法能夠藉助Object.is方法作到。
7.5 數組實例的fill()
fill方法使用給定值,填充一個數組。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
上面代碼代表,fill方法用於空數組的初始化很是方便。數組中已有的元素,會被所有抹去。
fill方法還能夠接受第二個和第三個參數,用於指定填充的起始位置和結束位置。
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
上面代碼表示,fill方法從1號位開始,向原數組填充7,到2號位以前結束。
7.6 數組實例的entries(),keys()和values()
ES6提供三個新的方法——entries(),keys()和values()——用於遍歷數組。它們都返回一個遍歷器對象(詳見《Iterator》一章),能夠
用for...of循環進行遍歷,惟一的區別是keys()是對鍵名的遍歷、values()是對鍵值的遍歷,entries()是對鍵值對的遍歷。
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
若是不使用for...of循環,能夠手動調用遍歷器對象的next方法,進行遍歷。
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
7.7 數組實例的includes()
Array.prototype.includes方法返回一個布爾值,表示某個數組是否包含給定的值,與字符串的includes方法相似。該方法屬於ES7,但Babel轉碼器
已經支持。
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true
該方法的第二個參數表示搜索的起始位置,默認爲0。若是第二個參數爲負數,則表示倒數的位置,若是這時它大於數組長度(好比第二個參數爲-4,
但數組長度爲3),則會重置爲從0開始。
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
沒有該方法以前,咱們一般使用數組的indexOf方法,檢查是否包含某個值。
if (arr.indexOf(el) !== -1) {
// ...
}
indexOf方法有兩個缺點,一是不夠語義化,它的含義是找到參數值的第一個出現位置,因此要去比較是否不等於-1,表達起來不夠直觀。二是,它內
部使用嚴格至關運算符(===)進行判斷,這會致使對NaN的誤判。
[NaN].indexOf(NaN)
// -1
includes使用的是不同的判斷算法,就沒有這個問題。
[NaN].includes(NaN)
// true
下面代碼用來檢查當前環境是否支持該方法,若是不支持,部署一個簡易的替代版本。
const contains = (() =>
Array.prototype.includes
? (arr, value) => arr.includes(value)
: (arr, value) => arr.some(el => el === value)
)();
contains(["foo", "bar"], "baz"); // => false
另外,Map和Set數據結構有一個has方法,須要注意與includes區分。
Map結構的has方法,是用來查找鍵名的,比
如Map.prototype.has(key)、WeakMap.prototype.has(key)、Reflect.has(target, propertyKey)。
Set結構的has方法,是用來查找值的,好比Set.prototype.has(value)、WeakSet.prototype.has(value)。
7.8 數組的空位
數組的空位指,數組的某一個位置沒有任何值。好比,Array構造函數返回的數組都是空位。
Array(3) // [, , ,]
上面代碼中,Array(3)返回一個具備3個空位的數組。
注意,空位不是undefined,一個位置的值等於undefined,依然是有值的。空位是沒有任何值,in運算符能夠說明這一點。
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
上面代碼說明,第一個數組的0號位置是有值的,第二個數組的0號位置沒有值。
ES5對空位的處理,已經很不一致了,大多數狀況下會忽略空位。
forEach(), filter(), every() 和some()都會跳過空位。
map()會跳過空位,但會保留這個值
join()和toString()會將空位視爲undefined,而undefined和null會被處理成空字符串。
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1
// filter方法
['a',,'b'].filter(x => true) // ['a','b']
// every方法
[,'a'].every(x => x==='a') // true
// some方法
[,'a'].some(x => x !== 'a') // false
// map方法
[,'a'].map(x => 1) // [,1]
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
ES6則是明確將空位轉爲undefined。
Array.from方法會將數組的空位,轉爲undefined,也就是說,這個方法不會忽略空位。
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
擴展運算符(...)也會將空位轉爲undefined。
[...['a',,'b']]
// [ "a", undefined, "b" ]
copyWithin()會連空位一塊兒拷貝。
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
fill()會將空位視爲正常的數組位置。
new Array(3).fill('a') // ["a","a","a"]
for...of循環也會遍歷空位。
let arr = [, ,];
for (let i of arr) {
console.log(1);
}
// 1
// 1
上面代碼中,數組arr有兩個空位,for...of並無忽略它們。若是改爲map方法遍歷,空位是會跳過的。
entries()、keys()、values()、find()和findIndex()會將空位處理成undefined。
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]
// keys()
[...[,'a'].keys()] // [0,1]
// values()
[...[,'a'].values()] // [undefined,"a"]
// find()
[,'a'].find(x => true) // undefined
// findIndex()
[,'a'].findIndex(x => true) // 0
因爲空位的處理規則很是不統一,因此建議避免出現空位。
8 函數的擴展
8.1 函數參數的默認值
8.1.1 基本用法
在ES6以前,不能直接爲函數的參數指定默認值,只能採用變通的方法。
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面代碼檢查函數log的參數y有沒有賦值,若是沒有,則指定默認值爲World。這種寫法的缺點在於,若是參數y賦值了,可是對應的布爾值
爲false,則該賦值不起做用。就像上面代碼的最後一行,參數y等於空字符,結果被改成默認值。
爲了不這個問題,一般須要先判斷一下參數y是否被賦值,若是沒有,再等於默認值。
if (typeof y === 'undefined') {
y = 'World';
}
ES6容許爲函數的參數設置默認值,即直接寫在參數定義的後面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
能夠看到,ES6的寫法比ES5簡潔許多,並且很是天然。下面是另外一個例子。
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
var p = new Point();
p // { x: 0, y: 0 }
除了簡潔,ES6的寫法還有兩個好處:首先,閱讀代碼的人,能夠馬上意識到哪些參數是能夠省略的,不用查看函數體或文檔;其次,有利於未來的代
碼優化,即便將來的版本在對外接口中,完全拿掉這個參數,也不會致使之前的代碼沒法運行。
參數變量是默認聲明的,因此不能用let或const再次聲明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
上面代碼中,參數變量x是默認聲明的,在函數體中,不能用let或const再次聲明,不然會報錯。
8.1.2 與解構賦值默認值結合使用
參數默認值能夠與解構賦值的默認值,結合起來使用。
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined
上面代碼使用了對象的解構賦值默認值,而沒有使用函數參數的默認值。只有當函數foo的參數是一個對象時,變量x和y纔會經過解構賦值而生成。如
果函數foo調用時參數不是對象,變量x和y就不會生成,從而報錯。若是參數對象沒有y屬性,y的默認值5纔會生效。
下面是另外一個對象的解構賦值默認值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 報錯
上面代碼中,若是函數fetch的第二個參數是一個對象,就能夠爲它的三個屬性設置默認值。
上面的寫法不能省略第二個參數,若是結合函數參數的默認值,就能夠省略第二個參數。這時,就出現了雙重默認值。
function fetch(url, { method = 'GET' } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
上面代碼中,函數fetch沒有第二個參數時,函數參數的默認值就會生效,而後纔是解構賦值的默認值生效,變量method纔會取到默認值GET。
再請問下面兩種寫法有什麼差異?
// 寫法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
上面兩種寫法都對函數的參數設定了默認值,區別是寫法一函數參數的默認值是空對象,可是設置了對象解構賦值的默認值;寫法二函數參數的默認
值是一個有具體屬性的對象,可是沒有設置對象解構賦值的默認值。
// 函數沒有參數的狀況
m1() // [0, 0]
m2() // [0, 0]
// x和y都有值的狀況
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x有值,y無值的狀況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x和y都無值的狀況
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
8.1.3 參數默認值的位置
一般狀況下,定義了默認值的參數,應該是函數的尾參數。由於這樣比較容易看出來,到底省略了哪些參數。若是非尾部的參數設置默認值,實際上
這個參數是無法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]
上面代碼中,有默認值的參數都不是尾參數。這時,沒法只省略該參數,而不省略它後面的參數,除非顯式輸入undefined。
若是傳入undefined,將觸發該參數等於默認值,null則沒有這個效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
上面代碼中,x參數對應undefined,結果觸發了默認值,y參數等於null,就沒有觸發默認值。
8.1.4 函數的length屬性
指定了默認值之後,函數的length屬性,將返回沒有指定默認值的參數個數。也就是說,指定了默認值後,length屬性將失真。
(function (a) {}).length // 1
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
上面代碼中,length屬性的返回值,等於函數的參數個數減去指定了默認值的參數個數。好比,上面最後一個函數,定義了3個參數,其中有一個參
數c指定了默認值,所以length屬性等於3減去1,最後獲得2。
這是由於length屬性的含義是,該函數預期傳入的參數個數。某個參數指定默認值之後,預期傳入的參數個數就不包括這個參數了。同理,rest參數也
不會計入length屬性。
(function(...args) {}).length // 0
若是設置了默認值的參數不是尾參數,那麼length屬性也再也不計入後面的參數了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
8.1.5 做用域
一個須要注意的地方是,若是參數默認值是一個變量,則該變量所處的做用域,與其餘變量的做用域規則是同樣的,即先是當前函數的做用域,而後
纔是全局做用域。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
上面代碼中,參數y的默認值等於x。調用時,因爲函數做用域內部的變量x已經生成,因此y等於參數x,而不是全局變量x。
若是調用時,函數做用域內部的變量x沒有生成,結果就會不同。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面代碼中,函數調用時,y的默認值變量x還沒有在函數內部生成,因此x指向全局變量。
若是此時,全局變量x不存在,就會報錯。
function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
下面這樣寫,也會報錯。
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
上面代碼中,函數foo的參數x的默認值也是x。這時,默認值x的做用域是函數做用域,而不是全局做用域。因爲在函數做用域中,存在變量x,可是
默認值在x賦值以前先執行了,因此這時屬於暫時性死區(參見《let和const命令》一章),任何對x的操做都會報錯。
若是參數的默認值是一個函數,該函數的做用域是其聲明時所在的做用域。請看下面的例子。
let foo = 'outer';
function bar(func = x => foo) {
let foo = 'inner';
console.log(func()); // outer
}
bar();
上面代碼中,函數bar的參數func的默認值是一個匿名函數,返回值爲變量foo。這個匿名函數聲明時,bar函數的做用域尚未造成,因此匿名函數
裏面的foo指向外層做用域的foo,輸出outer。
若是寫成下面這樣,就會報錯。
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar() // ReferenceError: foo is not defined
上面代碼中,匿名函數裏面的foo指向函數外層,可是函數外層並無聲明foo,因此就報錯了。
下面是一個更復雜的例子。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
上面代碼中,函數foo的參數y的默認值是一個匿名函數。函數foo調用時,它的參數x的值爲undefined,因此y函數內部的x一開始是undefined,後
來被從新賦值2。可是,函數foo內部從新聲明瞭一個x,值爲3,這兩個x是不同的,互相不產生影響,所以最後輸出3。
若是將var x = 3的var去除,兩個x就是同樣的,最後輸出的就是2。
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}
foo() // 2
8.1.6 應用
利用參數默認值,能夠指定某一個參數不得省略,若是省略就拋出一個錯誤。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
上面代碼的foo函數,若是調用的時候沒有參數,就會調用默認值throwIfMissing函數,從而拋出一個錯誤。
從上面代碼還能夠看到,參數mustBeProvided的默認值等於throwIfMissing函數的運行結果(即函數名以後有一對圓括號),這代表參數的默認值不
是在定義時執行,而是在運行時執行(即若是參數已經賦值,默認值中的函數就不會運行),這與python語言不同。
另外,能夠將參數默認值設爲undefined,代表這個參數是能夠省略的。
function foo(optional = undefined) { ··· }
8.2 rest參數
ES6引入rest參數(形式爲「...變量名」),用於獲取函數的多餘參數,這樣就不須要使用arguments對象了。rest參數搭配的變量是一個數組,該變量將
多餘的參數放入數組中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
上面代碼的add函數是一個求和函數,利用rest參數,能夠向該函數傳入任意數目的參數。
下面是一個rest參數代替arguments變量的例子。
// arguments變量的寫法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest參數的寫法
const sortNumbers = (...numbers) => numbers.sort();
上面代碼的兩種寫法,比較後能夠發現,rest參數的寫法更天然也更簡潔。
rest參數中的變量表明一個數組,因此數組特有的方法均可以用於這個變量。下面是一個利用rest參數改寫數組push方法的例子。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
注意,rest參數以後不能再有其餘參數(即只能是最後一個參數),不然會報錯。
// 報錯
function f(a, ...b, c) {
// ...
}
函數的length屬性,不包括rest參數。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
8.3 擴展運算符
8.3.1 含義
擴展運算符(spread)是三個點(...)。它比如rest參數的逆運算,將一個數組轉爲用逗號分隔的參數序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
該運算符主要用於函數調用。
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
var numbers = [4, 38];
add(...numbers) // 42
上面代碼中,array.push(...items)和add(...numbers)這兩行,都是函數的調用,它們的都使用了擴展運算符。該運算符將一個數組,變爲參數序
列。
擴展運算符與正常的函數參數能夠結合使用,很是靈活。
function f(v, w, x, y, z) { }
var args = [0, 1];
f(-1, ...args, 2, ...[3]);
8.3.2 替代數組的apply方法
因爲擴展運算符能夠展開數組,因此再也不須要apply方法,將數組轉爲函數的參數了。
// ES5的寫法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的寫法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f(...args);
下面是擴展運算符取代apply方法的一個實際的例子,應用Math.max方法,簡化求出一個數組最大元素的寫法。
// ES5的寫法
Math.max.apply(null, [14, 3, 77])
// ES6的寫法
Math.max(...[14, 3, 77])
// 等同於
Math.max(14, 3, 77);
上面代碼表示,因爲JavaScript不提供求數組最大元素的函數,因此只能套用Math.max函數,將數組轉爲一個參數序列,而後求最大值。有了擴展運算
符之後,就能夠直接用Math.max了。
另外一個例子是經過push函數,將一個數組添加到另外一個數組的尾部。
// ES5的寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6的寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
上面代碼的ES5寫法中,push方法的參數不能是數組,因此只好經過apply方法變通使用push方法。有了擴展運算符,就能夠直接將數組傳入push方
法。
下面是另一個例子。
// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);
8.3.3 擴展運算符的應用
(1)合併數組
擴展運算符提供了數組合並的新寫法。
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合併數組
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合併數組
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
(2)與解構賦值結合
擴展運算符能夠與解構賦值結合起來,用於生成數組。
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
下面是另一些例子。
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []:
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
若是將擴展運算符用於數組賦值,只能放在參數的最後一位,不然會報錯。
const [...butLast, last] = [1, 2, 3, 4, 5];
// 報錯
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 報錯
(3)函數的返回值
JavaScript的函數只能返回一個值,若是須要返回多個值,只能返回數組或對象。擴展運算符提供瞭解決這個問題的一種變通方法。
var dateFields = readDateFields(database);
var d = new Date(...dateFields);
上面代碼從數據庫取出一行數據,經過擴展運算符,直接將其傳入構造函數Date。
(4)字符串
擴展運算符還能夠將字符串轉爲真正的數組。
[...'hello']
// [ "h", "e", "l", "l", "o" ]
上面的寫法,有一個重要的好處,那就是可以正確識別32位的Unicode字符。
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
上面代碼的第一種寫法,JavaScript會將32位Unicode字符,識別爲2個字符,採用擴展運算符就沒有這個問題。所以,正確返回字符串長度的函數,
能夠像下面這樣寫。
function length(str) {
return [...str].length;
}
length('x\uD83D\uDE80y') // 3
凡是涉及到操做32位Unicode字符的函數,都有這個問題。所以,最好都用擴展運算符改寫。
let str = 'x\uD83D\uDE80y';
str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'
[...str].reverse().join('')
// 'y\uD83D\uDE80x'
上面代碼中,若是不用擴展運算符,字符串的reverse操做就不正確。
(5)實現了Iterator接口的對象
任何Iterator接口的對象,均可以用擴展運算符轉爲真正的數組。
var nodeList = document.querySelectorAll('div');
var array = [...nodeList];
上面代碼中,querySelectorAll方法返回的是一個nodeList對象。它不是數組,而是一個相似數組的對象。這時,擴展運算符能夠將其轉爲真正的數
組,緣由就在於NodeList對象實現了Iterator接口。
對於那些沒有部署Iterator接口的相似數組的對象,擴展運算符就沒法將其轉爲真正的數組。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];
上面代碼中,arrayLike是一個相似數組的對象,可是沒有部署Iterator接口,擴展運算符就會報錯。這時,能夠改成使用Array.from方法
將arrayLike轉爲真正的數組。
(6)Map和Set結構,Generator函數
擴展運算符內部調用的是數據結構的Iterator接口,所以只要具備Iterator接口的對象,均可以使用擴展運算符,好比Map結構。
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
Generator函數運行後,返回一個遍歷器對象,所以也能夠使用擴展運算符。
var go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
上面代碼中,變量go是一個Generator函數,執行後返回的是一個遍歷器對象,對這個遍歷器對象執行擴展運算符,就會將內部遍歷獲得的值,轉爲一
個數組。
若是對沒有iterator接口的對象,使用擴展運算符,將會報錯。
var obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object
8.4 name屬性
函數的name屬性,返回該函數的函數名。
function foo() {}
foo.name // "foo"
這個屬性早就被瀏覽器普遍支持,可是直到ES6,纔將其寫入了標準。
須要注意的是,ES6對這個屬性的行爲作出了一些修改。若是將一個匿名函數賦值給一個變量,ES5的name屬性,會返回空字符串,而ES6的name屬性
會返回實際的函數名。
var func1 = function () {};
// ES5
func1.name // ""
// ES6
func1.name // "func1"
上面代碼中,變量func1等於一個匿名函數,ES5和ES6的name屬性返回的值不同。
若是將一個具名函數賦值給一個變量,則ES5和ES6的name屬性都返回這個具名函數本來的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
Function構造函數返回的函數實例,name屬性的值爲「anonymous」。
(new Function).name // "anonymous"
bind返回的函數,name屬性值會加上「bound 」前綴。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
8.5 箭頭函數
8.5.1 基本用法
ES6容許使用「箭頭」(=>)定義函數。
var f = v => v;
上面的箭頭函數等同於:
var f = function(v) {
return v;
};
若是箭頭函數不須要參數或須要多個參數,就使用一個圓括號表明參數部分。
var f = () => 5;
// 等同於
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同於
var sum = function(num1, num2) {
return num1 + num2;
};
若是箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,而且使用return語句返回。
var sum = (num1, num2) => { return num1 + num2; }
因爲大括號被解釋爲代碼塊,因此若是箭頭函數直接返回一個對象,必須在對象外面加上括號。
var getTempItem = id => ({ id: id, name: "Temp" });
箭頭函數能夠與變量解構結合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同於
function full(person) {
return person.first + ' ' + person.last;
}
箭頭函數使得表達更加簡潔。
const isEven = n => n % 2 == 0;
const square = n => n * n;
上面代碼只用了兩行,就定義了兩個簡單的工具函數。若是不用箭頭函數,可能就要佔用多行,並且還不如如今這樣寫醒目。
箭頭函數的一個用處是簡化回調函數。
// 正常函數寫法
[1,2,3].map(function (x) {
return x * x;
});
// 箭頭函數寫法
[1,2,3].map(x => x * x);
另外一個例子是
// 正常函數寫法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭頭函數寫法
var result = values.sort((a, b) => a - b);
下面是rest參數與箭頭函數結合的例子。
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
8.5.2 使用注意點
箭頭函數有幾個使用注意點。
(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。
(2)不能夠看成構造函數,也就是說,不能夠使用new命令,不然會拋出一個錯誤。
(3)不能夠使用arguments對象,該對象在函數體內不存在。若是要用,能夠用Rest參數代替。
(4)不能夠使用yield命令,所以箭頭函數不能用做Generator函數。
上面四點中,第一點尤爲值得注意。this對象的指向是可變的,可是在箭頭函數中,它是固定的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面代碼中,setTimeout的參數是一個箭頭函數,這個箭頭函數的定義生效是在foo函數生成時,而它的真正執行要等到100毫秒後。若是是普通函
數,執行時this應該指向全局對象window,這時應該輸出21。可是,箭頭函數致使this老是指向函數定義生效時所在的對象(本例是{id: 42}),所
以輸出的是42。
箭頭函數可讓setTimeout裏面的this,綁定定義時所在的做用域,而不是指向運行時所在的做用域。下面是另外一個例子。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭頭函數
setInterval(() => this.s1++, 1000);
// 普通函數
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
上面代碼中,Timer函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this綁定定義時所在的做用域(即Timer函數),後者
的this指向運行時所在的做用域(即全局對象)。因此,3100毫秒以後,timer.s1被更新了3次,而timer.s2一次都沒更新。
箭頭函數可讓this指向固定化,這種特性頗有利於封裝回調函數。下面是一個例子,DOM事件的回調函數封裝在一個對象裏面。
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};
上面代碼的init方法中,使用了箭頭函數,這致使這個箭頭函數裏面的this,老是指向handler對象。不然,回調函數運行時,this.doSomething這
一行會報錯,由於此時this指向document對象。
this指向的固定化,並非由於箭頭函數內部有綁定this的機制,實際緣由是箭頭函數根本沒有本身的this,致使內部的this就是外層代碼塊
的this。正是由於它沒有this,因此也就不能用做構造函數。
因此,箭頭函數轉成ES5的代碼以下。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
上面代碼中,轉換後的ES5版本清楚地說明了,箭頭函數裏面根本沒有本身的this,而是引用外層的this。
請問下面的代碼之中有幾個this?
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
上面代碼之中,只有一個this,就是函數foo的this,因此t一、t二、t3都輸出一樣的結果。由於全部的內層函數都是箭頭函數,都沒有本身的this,
它們的this其實都是最外層foo函數的this。
除了this,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:arguments、super、new.target。
function foo() {
setTimeout(() => {
console.log('args:', arguments);
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
上面代碼中,箭頭函數內部的變量arguments,實際上是函數foo的arguments變量。
另外,因爲箭頭函數沒有本身的this,因此固然也就不能用call()、apply()、bind()這些方法去改變this的指向。
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
上面代碼中,箭頭函數沒有本身的this,因此bind方法無效,內部的this指向外部的this。
長期以來,JavaScript語言的this對象一直是一個使人頭痛的問題,在對象方法中使用this,必須很是當心。箭頭函數」綁定」this,很大程度上解決了
這個困擾。
8.5.3 嵌套的箭頭函數
箭頭函數內部,還能夠再使用箭頭函數。下面是一個ES5語法的多重嵌套函數。
function insert(value) {
return {into: function (array) {
return {after: function (afterValue) {
array.splice(array.indexOf(afterValue) + 1, 0, value);
return array;
}};
}};
}
insert(2).into([1, 3]).after(1); //[1, 2, 3]
上面這個函數,能夠使用箭頭函數改寫。
let insert = (value) => ({into: (array) => ({after: (afterValue) => {
array.splice(array.indexOf(afterValue) + 1, 0, value);
return array;
}})});
insert(2).into([1, 3]).after(1); //[1, 2, 3]
下面是一個部署管道機制(pipeline)的例子,即前一個函數的輸出是後一個函數的輸入。
const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val);
const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);
addThenMult(5)
// 12
若是以爲上面的寫法可讀性比較差,也能夠採用下面的寫法。
const plus1 = a => a + 1;
const mult2 = a => a * 2;
mult2(plus1(5))
// 12
箭頭函數還有一個功能,就是能夠很方便地改寫λ演算。
// λ演算的寫法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
// ES6的寫法
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
上面兩種寫法,幾乎是一一對應的。因爲λ演算對於計算機科學很是重要,這使得咱們能夠用ES6做爲替代工具,探索計算機科學。
8.6 函數綁定
箭頭函數能夠綁定this對象,大大減小了顯式綁定this對象的寫法(call、apply、bind)。可是,箭頭函數並不適用於全部場合,因此ES7提出
了「函數綁定」(function bind)運算符,用來取代call、apply、bind調用。雖然該語法仍是ES7的一個提案,可是Babel轉碼器已經支持。
函數綁定運算符是並排的兩個雙冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,做爲上下文環境(即this對
象),綁定到右邊的函數上面。
foo::bar;
// 等同於
bar.bind(foo);
foo::bar(...arguments);
// 等同於
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}
若是雙冒號左邊爲空,右邊是一個對象的方法,則等於將該方法綁定在該對象上面。
var method = obj::obj.foo;
// 等同於
var method = ::obj.foo;
let log = ::console.log;
// 等同於
var log = console.log.bind(console);
因爲雙冒號運算符返回的仍是原對象,所以能夠採用鏈式寫法。
// 例一
import { map, takeWhile, forEach } from "iterlib";
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
// 例二
let { find, html } = jake;
document.querySelectorAll("div.myClass")
::find("p")
::html("hahaha");
8.7 尾調用優化
8.7.1 什麼是尾調用?
尾調用(Tail Call)是函數式編程的一個重要概念,自己很是簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另外一個函數。
function f(x){
return g(x);
}
上面代碼中,函數f的最後一步是調用函數g,這就叫尾調用。
如下三種狀況,都不屬於尾調用。
// 狀況一
function f(x){
let y = g(x);
return y;
}
// 狀況二
function f(x){
return g(x) + 1;
}
// 狀況三
function f(x){
g(x);
}
上面代碼中,狀況一是調用函數g以後,還有賦值操做,因此不屬於尾調用,即便語義徹底同樣。狀況二也屬於調用後還有操做,即便寫在一行內。情
況三等同於下面的代碼。
function f(x){
g(x);
return undefined;
}
尾調用不必定出如今函數尾部,只要是最後一步操做便可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
上面代碼中,函數m和n都屬於尾調用,由於它們都是函數f的最後一步操做。
8.7.2 尾調用優化
尾調用之因此與其餘調用不一樣,就在於它的特殊的調用位置。
咱們知道,函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,
那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一
個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)。
尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調
用幀,取代外層函數的調用幀就能夠了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同於
function f() {
return g(3);
}
f();
// 等同於
g(3);
上面代碼中,若是函數g不是尾調用,函數f就須要保存內部變量m和n的值、g的調用位置等信息。但因爲調用g以後,函數f就結束了,因此執行到最後
一步,徹底能夠刪除 f(x) 的調用幀,只保留 g(3) 的調用幀。
這就叫作「尾調用優化」(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有
一項,這將大大節省內存。這就是「尾調用優化」的意義。
注意,只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行「尾調用優化」。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
上面的函數不會進行尾調用優化,由於內層函數inner用到了外層函數addOne的內部變量one。
8.7.3 尾遞歸
函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。
遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用
幀,因此永遠不會發生「棧溢出」錯誤。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面代碼是一個階乘函數,計算n的階乘,最多須要保存n個調用記錄,複雜度 O(n) 。
若是改寫成尾遞歸,只保留一個調用記錄,複雜度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
還有一個比較著名的例子,就是計算fibonacci 數列,也能充分說明尾遞歸優化的重要性
若是是非尾遞歸的fibonacci 遞歸方法
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10); // 89
// Fibonacci(100)
// Fibonacci(500)
// 堆棧溢出了
若是咱們使用尾遞歸優化過的fibonacci 遞歸算法
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
因而可知,「尾調用優化」對遞歸操做意義重大,因此一些函數式編程語言將其寫入了語言規格。ES6也是如此,第一次明確規定,全部ECMAScript的實
現,都必須部署「尾調用優化」。這就是說,在ES6中,只要使用尾遞歸,就不會發生棧溢出,相對節省內存。
8.7.4 遞歸函數的改寫
尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。好比上面的
例子,階乘函數 factorial 須要用到一箇中間變量 total ,那就把這個中間變量改寫成函數的參數。這樣作的缺點就是不太直觀,第一眼很難看出來,爲
什麼計算5的階乘,須要傳入兩個參數5和1?
兩個方法能夠解決這個問題。方法一是在尾遞歸函數以外,再提供一個正常形式的函數。
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
function factorial(n) {
return tailFactorial(n, 1);
}
factorial(5) // 120
上面代碼經過一個正常形式的階乘函數 factorial ,調用尾遞歸函數 tailFactorial ,看起來就正常多了。
函數式編程有一個概念,叫作柯里化(currying),意思是將多參數的函數轉換成單參數的形式。這裏也能夠使用柯里化。
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n);
};
}
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
factorial(5) // 120
上面代碼經過柯里化,將尾遞歸函數 tailFactorial 變爲只接受1個參數的 factorial 。
第二種方法就簡單多了,就是採用ES6的函數默認值。
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
上面代碼中,參數 total 有默認值1,因此調用時不用提供這個值。
總結一下,遞歸本質上是一種循環操做。純粹的函數式編程語言沒有循環操做命令,全部的循環都用遞歸實現,這就是爲何尾遞歸對這些語言極其
重要。對於其餘支持「尾調用優化」的語言(好比Lua,ES6),只須要知道循環能夠用遞歸代替,而一旦使用遞歸,就最好使用尾遞歸。
8.7.5 嚴格模式
ES6的尾調用優化只在嚴格模式下開啓,正常模式是無效的。
這是由於在正常模式下,函數內部有兩個變量,能夠跟蹤函數的調用棧。
func.arguments:返回調用時函數的參數。
func.caller:返回調用當前函數的那個函數。
尾調用優化發生時,函數的調用棧會改寫,所以上面兩個變量就會失真。嚴格模式禁用這兩個變量,因此尾調用模式僅在嚴格模式下生效。
function restricted() {
"use strict";
restricted.caller; // 報錯
restricted.arguments; // 報錯
}
restricted();
8.7.6 尾遞歸優化的實現
尾遞歸優化只在嚴格模式下生效,那麼正常模式下,或者那些不支持該功能的環境中,有沒有辦法也使用尾遞歸優化呢?回答是能夠的,就是本身實
現尾遞歸優化。
它的原理很是簡單。尾遞歸之因此須要優化,緣由是調用棧太多,形成溢出,那麼只要減小調用棧,就不會溢出。怎麼作能夠減小調用棧呢?就是採
用「循環」換掉「遞歸」。
下面是一個正常的遞歸函數。
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)
上面代碼中,sum是一個遞歸函數,參數x是須要累加的值,參數y控制遞歸次數。一旦指定sum遞歸100000次,就會報錯,提示超出調用棧的最大次
數。
蹦牀函數(trampoline)能夠將遞歸執行轉爲循環執行。
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
上面就是蹦牀函數的一個實現,它接受一個函數f做爲參數。只要f執行後返回一個函數,就繼續執行。注意,這裏是返回一個函數,而後執行該函
數,而不是函數裏面調用函數,這樣就避免了遞歸執行,從而就消除了調用棧過大的問題。
而後,要作的就是將原來的遞歸函數,改寫爲每一步返回另外一個函數。
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}
上面代碼中,sum函數的每次執行,都會返回自身的另外一個版本。
如今,使用蹦牀函數執行sum,就不會發生調用棧溢出。
trampoline(sum(1, 100000))
// 100001
蹦牀函數並非真正的尾遞歸優化,下面的實現纔是。
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
// 100001
上面代碼中,tco函數是尾遞歸優化的實現,它的奧妙就在於狀態變量active。默認狀況下,這個變量是不激活的。一旦進入尾遞歸優化的過程,這個
變量就激活了。而後,每一輪遞歸sum返回的都是undefined,因此就避免了遞歸執行;而accumulated數組存放每一輪sum執行的參數,老是有值的,
這就保證了accumulator函數內部的while循環老是會執行。這樣就很巧妙地將「遞歸」改爲了「循環」,然後一輪的參數會取代前一輪的參數,保證了調用
棧只有一層。
8.8 函數參數的尾逗號
ES7有一個提案,容許函數的最後一個參數有尾逗號(trailing comma)。
目前,函數定義和調用時,都不容許有參數的尾逗號。
function clownsEverywhere(
param1,
param2
) { /* ... */ }
clownsEverywhere(
'foo',
'bar'
);
若是之後要在函數的定義之中添加參數,就勢必還要添加一個逗號。這對版本管理系統來講,就會顯示,添加逗號的那一行也發生了變更。這看上去
有點冗餘,所以新提案容許定義和調用時,尾部直接有一個逗號。
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
9 對象的擴展
9.1 屬性的簡潔表示法
ES6容許直接寫入變量和函數,做爲對象的屬性和方法。這樣的書寫更加簡潔。
var foo = 'bar';
var baz = {foo};
baz // {foo: "bar"}
// 等同於
var baz = {foo: foo};
上面代碼代表,ES6容許在對象之中,只寫屬性名,不寫屬性值。這時,屬性值等於屬性名所表明的變量。下面是另外一個例子。
function f(x, y) {
return {x, y};
}
// 等同於
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
除了屬性簡寫,方法也能夠簡寫。
var o = {
method() {
return "Hello!";
}
};
// 等同於
var o = {
method: function() {
return "Hello!";
}
};
下面是一個實際的例子。
var birth = '2000/01/01';
var Person = {
name: '張三',
//等同於birth: birth
birth,
// 等同於hello: function ()...
hello() { console.log('個人名字是', this.name); }
};
這種寫法用於函數的返回值,將會很是方便。
function getPoint() {
var x = 1;
var y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}
CommonJS模塊輸出變量,就很是合適使用簡潔寫法。
var ms = {};
function getItem (key) {
return key in ms ? ms[key] : null;
}
function setItem (key, value) {
ms[key] = value;
}
function clear () {
ms = {};
}
module.exports = { getItem, setItem, clear };
// 等同於
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
屬性的賦值器(setter)和取值器(getter),事實上也是採用這種寫法。
var cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('數值過小了!');
}
this._wheels = value;
}
}
注意,簡潔寫法的屬性名老是字符串,這會致使一些看上去比較奇怪的結果。
var obj = {
class () {}
};
// 等同於
var obj = {
'class': function() {}
};
上面代碼中,class是字符串,因此不會由於它屬於關鍵字,而致使語法解析報錯。
若是某個方法的值是一個Generator函數,前面須要加上星號。
var obj = {
* m(){
yield 'hello world';
}
};
9.2 屬性名錶達式
JavaScript語言定義對象的屬性,有兩種方法。
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
上面代碼的方法一是直接用標識符做爲屬性名,方法二是用表達式做爲屬性名,這時要將表達式放在方括號以內。
可是,若是使用字面量方式定義對象(使用大括號),在ES5中只能使用方法一(標識符)定義屬性。
var obj = {
foo: true,
abc: 123
};
ES6容許字面量定義對象時,用方法二(表達式)做爲對象的屬性名,即把表達式放在方括號內。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
下面是另外一個例子。
var lastWord = 'last word';
var a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
表達式還能夠用於定義方法名。
let obj = {
['h'+'ello']() {
return 'hi';
}
};
obj.hello() // hi
注意,屬性名錶達式與簡潔表示法,不能同時使用,會報錯。
// 報錯
var foo = 'bar';
var bar = 'abc';
var baz = { [foo] };
// 正確
var foo = 'bar';
var baz = { [foo]: 'abc'};
9.3 方法的name屬性
函數的name屬性,返回函數名。對象方法也是函數,所以也有name屬性。
var person = {
sayName() {
console.log(this.name);
},
get firstName() {
return "Nicholas";
}
};
person.sayName.name // "sayName"
person.firstName.name // "get firstName"
上面代碼中,方法的name屬性返回函數名(即方法名)。若是使用了取值函數,則會在方法名前加上get。若是是存值函數,方法名的前面會加
上set。
有兩種特殊狀況:bind方法創造的函數,name屬性返回「bound」加上原函數的名字;Function構造函數創造的函數,name屬性返回「anonymous」。
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
若是對象的方法是一個Symbol值,那麼name屬性返回的是這個Symbol值的描述。
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
上面代碼中,key1對應的Symbol值有描述,key2沒有。
9.4 Object.is()
ES5比較兩個值是否相等,只有兩個運算符:相等運算符(==)和嚴格相等運算符(===)。它們都有缺點,前者會自動轉換數據類型,後者的NaN不
等於自身,以及+0等於-0。JavaScript缺少一種運算,在全部環境中,只要兩個值是同樣的,它們就應該相等。
ES6提出「Same-value equality」(同值相等)算法,用來解決這個問題。Object.is就是部署這個算法的新方法。它用來比較兩個值是否嚴格相等,與
嚴格比較運算符(===)的行爲基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
不一樣之處只有兩個:一是+0不等於-0,二是NaN等於自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
ES5能夠經過下面的代碼,部署Object.is。
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 針對+0 不等於 -0的狀況
return x !== 0 || 1 / x === 1 / y;
}
// 針對NaN的狀況
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
9.5 Object.assign()
9.5.1 基本用法
Object.assign方法用於對象的合併,將源對象(source)的全部可枚舉屬性,複製到目標對象(target)。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign方法的第一個參數是目標對象,後面的參數都是源對象。
注意,若是目標對象與源對象有同名屬性,或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性。
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
若是隻有一個參數,Object.assign會直接返回該參數。
var obj = {a: 1};
Object.assign(obj) === obj // true
若是該參數不是對象,則會先轉成對象,而後返回。
typeof Object.assign(2) // "object"
因爲undefined和null沒法轉成對象,因此若是它們做爲參數,就會報錯。
Object.assign(undefined) // 報錯
Object.assign(null) // 報錯
若是非對象參數出如今源對象的位置(即非首參數),那麼處理規則有所不一樣。首先,這些參數都會轉成對象,若是沒法轉成對象,就會跳過。這意
味着,若是undefined和null不在首參數,就不會報錯。
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
其餘類型的值(即數值、字符串和布爾值)不在首參數,也不會報錯。可是,除了字符串會以數組形式,拷貝入目標對象,其餘值都不會產生效果。
var v1 = 'abc';
var v2 = true;
var v3 = 10;
var obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
上面代碼中,v一、v二、v3分別是字符串、布爾值和數值,結果只有字符串合入目標對象(以字符數組的形式),數值和布爾值都會被忽略。這是由於
只有字符串的包裝對象,會產生可枚舉屬性。
Object(true) // {[[PrimitiveValue]]: true}
Object(10) // {[[PrimitiveValue]]: 10}
Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
上面代碼中,布爾值、數值、字符串分別轉成對應的包裝對象,能夠看到它們的原始值都在包裝對象的內部屬性[[PrimitiveValue]]上面,這個屬性
是不會被Object.assign拷貝的。只有字符串的包裝對象,會產生可枚舉的實義屬性,那些屬性則會被拷貝。
Object.assign拷貝的屬性是有限制的,只拷貝源對象的自身屬性(不拷貝繼承屬性),也不拷貝不可枚舉的屬性(enumerable: false)。
Object.assign({b: 'c'},
Object.defineProperty({}, 'invisible', {
enumerable: false,
value: 'hello'
})
)
// { b: 'c' }
上面代碼中,Object.assign要拷貝的對象只有一個不可枚舉屬性invisible,這個屬性並無被拷貝進去。
屬性名爲Symbol值的屬性,也會被Object.assign拷貝。
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
9.5.2 注意點
Object.assign方法實行的是淺拷貝,而不是深拷貝。也就是說,若是源對象某個屬性的值是對象,那麼目標對象拷貝獲得的是這個對象的引用。
var obj1 = {a: {b: 1}};
var obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2
上面代碼中,源對象obj1的a屬性的值是一個對象,Object.assign拷貝獲得的是這個對象的引用。這個對象的任何變化,都會反映到目標對象上面。
對於這種嵌套的對象,一旦遇到同名屬性,Object.assign的處理方法是替換,而不是添加。
var target = { a: { b: 'c', d: 'e' } }
var source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
上面代碼中,target對象的a屬性被source對象的a屬性整個替換掉了,而不會獲得{ a: { b: 'hello', d: 'e' } }的結果。這一般不是開發者想要
的,須要特別當心。
有一些函數庫提供Object.assign的定製版本(好比Lodash的_.defaultsDeep方法),能夠解決淺拷貝的問題,獲得深拷貝的合併。
注意,Object.assign能夠用來處理數組,可是會把數組視爲對象。
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
上面代碼中,Object.assign把數組視爲屬性名爲0、一、2的對象,所以目標數組的0號屬性4覆蓋了原數組的0號屬性1。
9.5.3 常見用途
Object.assign方法有不少用處。
(1)爲對象添加屬性
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
上面方法經過Object.assign方法,將x屬性和y屬性添加到Point類的對象實例。
(2)爲對象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同於下面的寫法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
上面代碼使用了對象屬性的簡潔表示法,直接將兩個函數放在大括號中,再使用assign方法添加到SomeClass.prototype之中。
(3)克隆對象
function clone(origin) {
return Object.assign({}, origin);
}
上面代碼將原始對象拷貝到一個空對象,就獲得了原始對象的克隆。
不過,採用這種方法克隆,只能克隆原始對象自身的值,不能克隆它繼承的值。若是想要保持繼承鏈,能夠採用下面的代碼。
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
(4)合併多個對象
將多個對象合併到某個對象。
const merge =
(target, ...sources) => Object.assign(target, ...sources);
若是但願合併後返回一個新對象,能夠改寫上面函數,對一個空對象合併。
const merge =
(...sources) => Object.assign({}, ...sources);
(5)爲屬性指定默認值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
let options = Object.assign({}, DEFAULTS, options);
}
上面代碼中,DEFAULTS對象是默認值,options對象是用戶提供的參數。Object.assign方法將DEFAULTS和options合併成一個新對象,若是二者有同
名屬性,則option的屬性值會覆蓋DEFAULTS的屬性值。
注意,因爲存在深拷貝的問題,DEFAULTS對象和options對象的全部屬性的值,都只能是簡單類型,而不能指向另外一個對象。不然,將致使DEFAULTS對
象的該屬性不起做用。
9.6 屬性的可枚舉性
對象的每一個屬性都有一個描述對象(Descriptor),用來控制該屬性的行爲。Object.getOwnPropertyDescriptor方法能夠獲取該屬性的描述對象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
描述對象的enumerable屬性,稱爲」可枚舉性「,若是該屬性爲false,就表示某些操做會忽略當前屬性。
ES5有三個操做會忽略enumerable爲false的屬性。
for...in循環:只遍歷對象自身的和繼承的可枚舉的屬性
Object.keys():返回對象自身的全部可枚舉的屬性的鍵名
JSON.stringify():只串行化對象自身的可枚舉的屬性
ES6新增了一個操做Object.assign(),會忽略enumerable爲false的屬性,只拷貝對象自身的可枚舉的屬性。
這四個操做之中,只有for...in會返回繼承的屬性。實際上,引入enumerable的最初目的,就是讓某些屬性能夠規避掉for...in操做。好比,對象原
型的toString方法,以及數組的length屬性,就經過這種手段,不會被for...in遍歷到。
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
上面代碼中,toString和length屬性的enumerable都是false,所以for...in不會遍歷到這兩個繼承自原型的屬性。
另外,ES6規定,全部Class的原型的方法都是不可枚舉的。
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
總的來講,操做中引入繼承的屬性會讓問題複雜化,大多數時候,咱們只關心對象自身的屬性。因此,儘可能不要用for...in循環,而
用Object.keys()代替。
9.7 屬性的遍歷
ES6一共有5種方法能夠遍歷對象的屬性。
(1)for...in
for...in循環遍歷對象自身的和繼承的可枚舉屬性(不含Symbol屬性)。
(2)Object.keys(obj)
Object.keys返回一個數組,包括對象自身的(不含繼承的)全部可枚舉屬性(不含Symbol屬性)。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一個數組,包含對象自身的全部屬性(不含Symbol屬性,可是包括不可枚舉屬性)。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一個數組,包含對象自身的全部Symbol屬性。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一個數組,包含對象自身的全部屬性,無論是屬性名是Symbol或字符串,也無論是否可枚舉。
以上的5種方法遍歷對象的屬性,都遵照一樣的屬性遍歷的次序規則。
首先遍歷全部屬性名爲數值的屬性,按照數字排序。
其次遍歷全部屬性名爲字符串的屬性,按照生成時間排序。
最後遍歷全部屬性名爲Symbol值的屬性,按照生成時間排序。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
上面代碼中,Reflect.ownKeys方法返回一個數組,包含了參數對象的全部屬性。這個數組的屬性次序是這樣的,首先是數值屬性2和10,其次是字符
串屬性b和a,最後是Symbol屬性。
9.8 __proto__屬性,Object.setPrototypeOf(),Object.getPrototypeOf()
(1)__proto__屬性
__proto__屬性(先後各兩個下劃線),用來讀取或設置當前對象的prototype對象。目前,全部瀏覽器(包括IE11)都部署了這個屬性。
// es6的寫法
var obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;
// es5的寫法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };
該屬性沒有寫入ES6的正文,而是寫入了附錄,緣由是__proto__先後的雙下劃線,說明它本質上是一個內部屬性,而不是一個正式的對外的API,只
是因爲瀏覽器普遍支持,才被加入了ES6。標準明確規定,只有瀏覽器必須部署這個屬性,其餘運行環境不必定須要部署,並且新的代碼最好認爲這個
屬性是不存在的。所以,不管從語義的角度,仍是從兼容性的角度,都不要使用這個屬性,而是使用下面的Object.setPrototypeOf()(寫操
做)、Object.getPrototypeOf()(讀操做)、Object.create()(生成操做)代替。
在實現上,__proto__調用的是Object.prototype.__proto__,具體實現以下。
Object.defineProperty(Object.prototype, '__proto__', {
get() {
let _thisObj = Object(this);
return Object.getPrototypeOf(_thisObj);
},
set(proto) {
if (this === undefined || this === null) {
throw new TypeError();
}
if (!isObject(this)) {
return undefined;
}
if (!isObject(proto)) {
return undefined;
}
let status = Reflect.setPrototypeOf(this, proto);
if (!status) {
throw new TypeError();
}
},
});
function isObject(value) {
return Object(value) === value;
}
若是一個對象自己部署了__proto__屬性,則該屬性的值就是對象的原型。
Object.getPrototypeOf({ __proto__: null })
// null
(2)Object.setPrototypeOf()
Object.setPrototypeOf方法的做用與__proto__相同,用來設置一個對象的prototype對象。它是ES6正式推薦的設置原型對象的方法。
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
var o = Object.setPrototypeOf({}, null);
該方法等同於下面的函數。
function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
下面是一個例子。
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
上面代碼將proto對象設爲obj對象的原型,因此從obj對象能夠讀取proto對象的屬性。
(3)Object.getPrototypeOf()
該方法與setPrototypeOf方法配套,用於讀取一個對象的prototype對象。
Object.getPrototypeOf(obj);
下面是一個例子。
function Rectangle() {
}
var rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false
9.9 Object.values(),Object.entries()
9.9.1 Object.keys()
ES5引入了Object.keys方法,返回一個數組,成員是參數對象自身的(不含繼承的)全部可遍歷(enumerable)屬性的鍵名。
var obj = { foo: "bar", baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
目前,ES7有一個提案,引入了跟Object.keys配套的Object.values和Object.entries。
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
9.9.2 Object.values()
Object.values方法返回一個數組,成員是參數對象自身的(不含繼承的)全部可遍歷(enumerable)屬性的鍵值。
var obj = { foo: "bar", baz: 42 };
Object.values(obj)
// ["bar", 42]
返回數組的成員順序,與本章的《屬性的遍歷》部分介紹的排列規則一致。
var obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
上面代碼中,屬性名爲數值的屬性,是按照數值大小,從小到大遍歷的,所以返回的順序是b、c、a。
Object.values只返回對象自身的可遍歷屬性。
var obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []
上面代碼中,Object.create方法的第二個參數添加的對象屬性(屬性p),若是不顯式聲明,默認是不可遍歷的。Object.values不會返回這個屬性。
Object.values會過濾屬性名爲Symbol值的屬性。
Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']
若是Object.values方法的參數是一個字符串,會返回各個字符組成的一個數組。
Object.values('foo')
// ['f', 'o', 'o']
上面代碼中,字符串會先轉成一個相似數組的對象。字符串的每一個字符,就是該對象的一個屬性。所以,Object.values返回每一個屬性的鍵值,就是各
個字符組成的一個數組。
若是參數不是對象,Object.values會先將其轉爲對象。因爲數值和布爾值的包裝對象,都不會爲實例添加非繼承的屬性。因此,Object.values會返
回空數組。
Object.values(42) // []
Object.values(true) // []
9.9.3 Object.entries
Object.entries方法返回一個數組,成員是參數對象自身的(不含繼承的)全部可遍歷(enumerable)屬性的鍵值對數組。
var obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
除了返回值不同,該方法的行爲與Object.values基本一致。
若是原對象的屬性名是一個Symbol值,該屬性會被省略。
Object.entries({ [Symbol()]: 123, foo: 'abc' });
// [ [ 'foo', 'abc' ] ]
上面代碼中,原對象有兩個屬性,Object.entries只輸出屬性名非Symbol值的屬性。未來可能會有Reflect.ownEntries()方法,返回對象自身的全部
屬性。
Object.entries的基本用途是遍歷對象的屬性。
let obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`);
}
// "one": 1
// "two": 2
Object.entries方法的一個用處是,將對象轉爲真正的Map結構。
var obj = { foo: 'bar', baz: 42 };
var map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
本身實現Object.entries方法,很是簡單。
// Generator函數的版本
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
// 非Generator函數的版本
function entries(obj) {
let arr = [];
for (let key of Object.keys(obj)) {
arr.push([key, obj[key]]);
}
return arr;
}
9.10 對象的擴展運算符
目前,ES7有一個提案,將Rest解構賦值/擴展運算符(...)引入對象。Babel轉碼器已經支持這項功能。
(1)Rest解構賦值
對象的Rest解構賦值用於從一個對象取值,至關於將全部可遍歷的、但還沒有被讀取的屬性,分配到指定的對象上面。全部的鍵和它們的值,都會拷貝
到新對象上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
上面代碼中,變量z是Rest解構賦值所在的對象。它獲取等號右邊的全部還沒有讀取的鍵(a和b),將它們和它們的值拷貝過來。
因爲Rest解構賦值要求等號右邊是一個對象,因此若是等號右邊是undefined或null,就會報錯,由於它們沒法轉爲對象。
let { x, y, ...z } = null; // 運行時錯誤
let { x, y, ...z } = undefined; // 運行時錯誤
Rest解構賦值必須是最後一個參數,不然會報錯。
let { ...x, y, z } = obj; // 句法錯誤
let { x, ...y, ...z } = obj; // 句法錯誤
上面代碼中,Rest解構賦值不是最後一個參數,因此會報錯。
注意,Rest解構賦值的拷貝是淺拷貝,即若是一個鍵的值是複合類型的值(數組、對象、函數)、那麼Rest解構賦值拷貝的是這個值的引用,而不是
這個值的副本。
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
上面代碼中,x是Rest解構賦值所在的對象,拷貝了對象obj的a屬性。a屬性引用了一個對象,修改這個對象的值,會影響到Rest解構賦值對它的引
用。
另外,Rest解構賦值不會拷貝繼承自原型對象的屬性。
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let o3 = { ...o2 };
o3 // { b: 2 }
上面代碼中,對象o3是o2的拷貝,可是隻複製了o2自身的屬性,沒有複製它的原型對象o1的屬性。
下面是另外一個例子。
var o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...{ y, z } } = o;
x // 1
y // undefined
z // 3
上面代碼中,變量x是單純的解構賦值,因此能夠讀取繼承的屬性;Rest解構賦值產生的變量y和z,只能讀取對象自身的屬性,因此只有變量z能夠賦
值成功。
Rest解構賦值的一個用處,是擴展某個函數的參數,引入其餘操做。
function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// 使用x和y參數進行操做
// 其他參數傳給原始函數
return baseFunction(restConfig);
}
上面代碼中,原始函數baseFunction接受a和b做爲參數,函數wrapperFunction在baseFunction的基礎上進行了擴展,可以接受多餘的參數,而且保
留原始函數的行爲。
(2)擴展運算符
擴展運算符(...)用於取出參數對象的全部可遍歷屬性,拷貝到當前對象之中。
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
這等同於使用Object.assign方法。
let aClone = { ...a };
// 等同於
let aClone = Object.assign({}, a);
擴展運算符能夠用於合併兩個對象。
let ab = { ...a, ...b };
// 等同於
let ab = Object.assign({}, a, b);
若是用戶自定義的屬性,放在擴展運算符後面,則擴展運算符內部的同名屬性會被覆蓋掉。
let aWithOverrides = { ...a, x: 1, y: 2 };
// 等同於
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等同於
let x = 1, y = 2, aWithOverrides = { ...a, x, y };
// 等同於
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
上面代碼中,a對象的x屬性和y屬性,拷貝到新對象後會被覆蓋掉。
這用來修改現有對象部分的部分屬性就很方便了。
let newVersion = {
...previousVersion,
name: 'New Name' // Override the name property
};
上面代碼中,newVersion對象自定義了name屬性,其餘屬性所有複製自previousVersion對象。
若是把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。
let aWithDefaults = { x: 1, y: 2, ...a };
// 等同於
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同於
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
擴展運算符的參數對象之中,若是有取值函數get,這個函數是會執行的。
// 並不會拋出錯誤,由於x屬性只是被定義,但沒執行
let aWithXGetter = {
...a,
get x() {
throws new Error('not thrown yet');
}
};
// 會拋出錯誤,由於x屬性被執行了
let runtimeError = {
...a,
...{
get x() {
throws new Error('thrown now');
}
}
};
若是擴展運算符的參數是null或undefined,這個兩個值會被忽略,不會報錯。
let emptyObject = { ...null, ...undefined }; // 不報錯
9.11 Object.getOwnPropertyDescriptors()
ES5有一個Object.getOwnPropertyDescriptor方法,返回某個對象屬性的描述對象(descriptor)。
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
ES7有一個提案,提出了Object.getOwnPropertyDescriptors方法,返回指定對象全部自身屬性(非繼承屬性)的描述對象。
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
Object.getOwnPropertyDescriptors方法返回一個對象,全部原對象的屬性名都是該對象的屬性名,對應的屬性值就是該屬性的描述對象。
該方法的實現很是容易。
function getOwnPropertyDescriptors(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
該方法的提出目的,主要是爲了解決Object.assign()沒法正確拷貝get屬性和set屬性的問題。
const source = {
set foo(value) {
console.log(value);
}
};
const target1 = {};
Object.assign(target1, source);
Object.getOwnPropertyDescriptor(target1, 'foo')
// { value: undefined,
// writable: true,
// enumerable: true,
// configurable: true }
上面代碼中,source對象的foo屬性的值是一個賦值函數,Object.assign方法將這個屬性拷貝給target1對象,結果該屬性的值變成了undefined。這
是由於Object.assign方法老是拷貝一個屬性的值,而不會拷貝它背後的賦值方法或取值方法。
這時,Object.getOwnPropertyDescriptors方法配合Object.defineProperties方法,就能夠實現正確拷貝。
const source = {
set foo(value) {
console.log(value);
}
};
const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
// set: [Function: foo],
// enumerable: true,
// configurable: true }
上面代碼中,將兩個對象合併的邏輯提煉出來,就是下面這樣。
const shallowMerge = (target, source) => Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
Object.getOwnPropertyDescriptors方法的另外一個用處,是配合Object.create方法,將對象屬性克隆到一個新對象。這屬於淺拷貝。
const clone = Object.create(Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
// 或者
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
上面代碼會克隆對象obj。
另外,Object.getOwnPropertyDescriptors方法能夠實現,一個對象繼承另外一個對象。之前,繼承另外一個對象,經常寫成下面這樣。
const obj = {
__proto__: prot,
foo: 123,
};
ES6規定__proto__只有瀏覽器要部署,其餘環境不用部署。若是去除__proto__,上面代碼就要改爲下面這樣。
const obj = Object.create(prot);
obj.foo = 123;
// 或者
const obj = Object.assign(
Object.create(prot),
{
foo: 123,
}
);
有了Object.getOwnPropertyDescriptors,咱們就有了另外一種寫法。
const obj = Object.create(
prot,
Object.getOwnPropertyDescriptors({
foo: 123,
})
);
Object.getOwnPropertyDescriptors也能夠用來實現Mixin(混入)模式。
let mix = (object) => ({
with: (...mixins) => mixins.reduce(
(c, mixin) => Object.create(
c, Object.getOwnPropertyDescriptors(mixin)
), object)
});
// multiple mixins example
let a = {a: 'a'};
let b = {b: 'b'};
let c = {c: 'c'};
let d = mix(c).with(a, b);
上面代碼中,對象a和b被混入了對象c。
出於完整性的考慮,Object.getOwnPropertyDescriptors進入標準之後,還會有Reflect.getOwnPropertyDescriptors方法。
10 Symbol
10.1 概述
ES5的對象屬性名都是字符串,這容易形成屬性名的衝突。好比,你使用了一個他人提供的對象,但又想爲這個對象添加新的方法(mixin模式),新
方法的名字就有可能與現有方法產生衝突。若是有一種機制,保證每一個屬性的名字都是獨一無二的就行了,這樣就從根本上防止屬性名的衝突。這就
是ES6引入Symbol的緣由。
ES6引入了一種新的原始數據類型Symbol,表示獨一無二的值。它是JavaScript語言的第七種數據類型,前六種是:Undefined、Null、布爾值
(Boolean)、字符串(String)、數值(Number)、對象(Object)。
Symbol值經過Symbol函數生成。這就是說,對象的屬性名如今能夠有兩種類型,一種是原來就有的字符串,另外一種就是新增的Symbol類型。凡是屬性
名屬於Symbol類型,就都是獨一無二的,能夠保證不會與其餘屬性名產生衝突。
let s = Symbol();
typeof s
// "symbol"
上面代碼中,變量s就是一個獨一無二的值。typeof運算符的結果,代表變量s是Symbol數據類型,而不是字符串之類的其餘類型。
注意,Symbol函數前不能使用new命令,不然會報錯。這是由於生成的Symbol是一個原始類型的值,不是對象。也就是說,因爲Symbol值不是對象,
因此不能添加屬性。基本上,它是一種相似於字符串的數據類型。
Symbol函數能夠接受一個字符串做爲參數,表示對Symbol實例的描述,主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
上面代碼中,s1和s2是兩個Symbol值。若是不加參數,它們在控制檯的輸出都是Symbol(),不利於區分。有了參數之後,就等於爲它們加上了描述,
輸出的時候就可以分清,究竟是哪個值。
注意,Symbol函數的參數只是表示對當前Symbol值的描述,所以相同參數的Symbol函數的返回值是不相等的。
// 沒有參數的狀況
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有參數的狀況
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
上面代碼中,s1和s2都是Symbol函數的返回值,並且參數相同,可是它們是不相等的。
Symbol值不能與其餘類型的值進行運算,會報錯。
var sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
可是,Symbol值能夠顯式轉爲字符串。
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
另外,Symbol值也能夠轉爲布爾值,可是不能轉爲數值。
var sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
10.2 做爲屬性名的Symbol
因爲每個Symbol值都是不相等的,這意味着Symbol值能夠做爲標識符,用於對象的屬性名,就能保證不會出現同名的屬性。這對於一個對象由多個
模塊構成的狀況很是有用,能防止某一個鍵被不當心改寫或覆蓋。
var mySymbol = Symbol();
// 第一種寫法
var a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
var a = {
[mySymbol]: 'Hello!'
};
// 第三種寫法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上寫法都獲得一樣結果
a[mySymbol] // "Hello!"
上面代碼經過方括號結構和Object.defineProperty,將對象的屬性名指定爲一個Symbol值。
注意,Symbol值做爲對象屬性名時,不能用點運算符。
var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
上面代碼中,由於點運算符後面老是字符串,因此不會讀取mySymbol做爲標識名所指代的那個值,致使a的屬性名其實是一個字符串,而不是一個
Symbol值。
同理,在對象的內部,使用Symbol值定義屬性時,Symbol值必須放在方括號之中。
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
上面代碼中,若是s不放在方括號中,該屬性的鍵名就是字符串s,而不是s所表明的那個Symbol值。
採用加強的對象寫法,上面代碼的obj對象能夠寫得更簡潔一些。
let obj = {
[s](arg) { ... }
};
Symbol類型還能夠用於定義一組常量,保證這組常量的值都是不相等的。
Symbol類型還能夠用於定義一組常量,保證這組常量的值都是不相等的。
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
log(log.levels.DEBUG, 'debug message');
log(log.levels.INFO, 'info message');
下面是另一個例子。
const COLOR_RED = Symbol();
const COLOR_GREEN = Symbol();
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_GREEN:
return COLOR_RED;
default:
throw new Error('Undefined color');
}
}
常量使用Symbol值最大的好處,就是其餘任何值都不可能有相同的值了,所以能夠保證上面的switch語句會按設計的方式工做。
還有一點須要注意,Symbol值做爲屬性名時,該屬性仍是公開屬性,不是私有屬性。
10.3 實例:消除魔術字符串
魔術字符串指的是,在代碼之中屢次出現、與代碼造成強耦合的某一個具體的字符串或者數值。風格良好的代碼,應該儘可能消除魔術字符串,該由含
義清晰的變量代替。
function getArea(shape, options) {
var area = 0;
switch (shape) {
case 'Triangle': // 魔術字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 }); // 魔術字符串
上面代碼中,字符串「Triangle」就是一個魔術字符串。它屢次出現,與代碼造成「強耦合」,不利於未來的修改和維護。
經常使用的消除魔術字符串的方法,就是把它寫成一個變量。
var shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
var area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
上面代碼中,咱們把「Triangle」寫成shapeType對象的triangle屬性,這樣就消除了強耦合。
若是仔細分析,能夠發現shapeType.triangle等於哪一個值並不重要,只要確保不會跟其餘shapeType屬性的值衝突便可。所以,這裏就很適合改用
Symbol值。
const shapeType = {
triangle: Symbol()
};
上面代碼中,除了將shapeType.triangle的值設爲一個Symbol,其餘地方都不用修改。
10.4 屬性名的遍歷
Symbol做爲屬性名,該屬性不會出如今for...in、for...of循環中,也不會被Object.keys()、Object.getOwnPropertyNames()返回。可是,它也不
是私有屬性,有一個Object.getOwnPropertySymbols方法,能夠獲取指定對象的全部Symbol屬性名。
Object.getOwnPropertySymbols方法返回一個數組,成員是當前對象的全部用做屬性名的Symbol值。
var obj = {};
var a = Symbol('a');
var b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
var objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
下面是另外一個例子,Object.getOwnPropertySymbols方法與for...in循環、Object.getOwnPropertyNames方法進行對比的例子。
var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
for (var i in obj) {
console.log(i); // 無輸出
}
Object.getOwnPropertyNames(obj)
// []
Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]
上面代碼中,使用Object.getOwnPropertyNames方法得不到Symbol屬性名,須要使用Object.getOwnPropertySymbols方法。
另外一個新的API,Reflect.ownKeys方法能夠返回全部類型的鍵名,包括常規鍵名和Symbol鍵名。
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// [Symbol(my_key), 'enum', 'nonEnum']
因爲以Symbol值做爲名稱的屬性,不會被常規方法遍歷獲得。咱們能夠利用這個特性,爲對象定義一些非私有的、但又但願只用於內部的方法。
var size = Symbol('size');
class Collection {
constructor() {
this[size] = 0;
}
add(item) {
this[this[size]] = item;
this[size]++;
}
static sizeOf(instance) {
return instance[size];
}
}
var x = new Collection();
Collection.sizeOf(x) // 0
x.add('foo');
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
上面代碼中,對象x的size屬性是一個Symbol值,因此Object.keys(x)、Object.getOwnPropertyNames(x)都沒法獲取它。這就形成了一種非私有的內
部方法的效果。
10.5 Symbol.for(),Symbol.keyFor()
有時,咱們但願從新使用同一個Symbol值,Symbol.for方法能夠作到這一點。它接受一個字符串做爲參數,而後搜索有沒有以該參數做爲名稱的
Symbol值。若是有,就返回這個Symbol值,不然就新建並返回一個以該字符串爲名稱的Symbol值。
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
上面代碼中,s1和s2都是Symbol值,可是它們都是一樣參數的Symbol.for方法生成的,因此其實是同一個值。
Symbol.for()與Symbol()這兩種寫法,都會生成新的Symbol。它們的區別是,前者會被登記在全局環境中供搜索,後者不會。Symbol.for()不會每次
調用就返回一個新的Symbol類型的值,而是會先檢查給定的key是否已經存在,若是不存在纔會新建一個值。好比,若是你調用Symbol.for("cat")30
次,每次都會返回同一個Symbol值,可是調用Symbol("cat")30次,會返回30個不一樣的Symbol值。
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
上面代碼中,因爲Symbol()寫法沒有登記機制,因此每次調用都會返回一個不一樣的值。
Symbol.keyFor方法返回一個已登記的Symbol類型值的key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
上面代碼中,變量s2屬於未登記的Symbol值,因此返回undefined。
須要注意的是,Symbol.for爲Symbol值登記的名字,是全局環境的,能夠在不一樣的iframe或service worker中取到同一個值。
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true
上面代碼中,iframe窗口生成的Symbol值,能夠在主頁面獲得。
10.6 實例:模塊的 Singleton 模式
Singleton模式指的是調用一個類,任什麼時候候返回的都是同一個實例。
對於 Node 來講,模塊文件能夠當作是一個類。怎麼保證每次執行這個模塊文件,返回的都是同一個實例呢?
很容易想到,能夠把實例放到頂層對象global。
// mod.js
function A() {
this.foo = 'hello';
}
if (!global._foo) {
global._foo = new A();
}
module.exports = global._foo;
而後,加載上面的mod.js。
var a = require('./mod.js');
console.log(a.foo);
上面代碼中,變量a任什麼時候候加載的都是A的同一個實例。
可是,這裏有一個問題,全局變量global._foo是可寫的,任何文件均可以修改。
var a = require('./mod.js');
global._foo = 123;
上面的代碼,會使得別的腳本加載mod.js都失真。
爲了防止這種狀況出現,咱們就能夠使用Symbol。
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];
上面代碼中,能夠保證global[FOO_KEY]不會被其餘腳本改寫。
10.7 內置的Symbol值
除了定義本身使用的Symbol值之外,ES6還提供了11個內置的Symbol值,指向語言內部使用的方法。
10.7.1 Symbol.hasInstance
對象的Symbol.hasInstance屬性,指向一個內部方法。當其餘對象使用instanceof運算符,判斷是否爲該對象的實例時,會調用這個方法。比
如,foo instanceof Foo在語言內部,實際調用的是Foo[Symbol.hasInstance](foo)。
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
上面代碼中,MyClass是一個類,new MyClass()會返回一個實例。該實例的Symbol.hasInstance方法,會在進行instanceof運算時自動調用,判斷左
側的運算子是否爲Array的實例。
下面是另外一個例子。
class Even {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}
1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false
10.7.2 Symbol.isConcatSpreadable
對象的Symbol.isConcatSpreadable屬性等於一個布爾值,表示該對象使用Array.prototype.concat()時,是否能夠展開。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
上面代碼說明,數組的默認行爲是能夠展開。Symbol.isConcatSpreadable屬性等於true或undefined,都有這個效果。
相似數組的對象也能夠展開,但它的Symbol.isConcatSpreadable屬性默認爲false,必須手動打開。
let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
對於一個類來講,Symbol.isConcatSpreadable屬性必須寫成實例的屬性。
class A1 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class A2 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = false;
}
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2)
// [1, 2, 3, 4, [5, 6]]
上面代碼中,類A1是可展開的,類A2是不可展開的,因此使用concat時有不同的結果。
10.7.3 Symbol.species
對象的Symbol.species屬性,指向一個方法。該對象做爲構造函數創造實例時,會調用這個方法。即若是this.constructor[Symbol.species]存在,
就會使用這個屬性做爲構造函數,來創造新的實例對象。
Symbol.species屬性默認的讀取器以下。
static get [Symbol.species]() {
return this;
}
10.7.4 Symbol.match
對象的Symbol.match屬性,指向一個函數。當執行str.match(myObject)時,若是該屬性存在,會調用它,返回該方法的返回值。
String.prototype.match(regexp)
// 等同於
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}
'e'.match(new MyMatcher()) // 1
10.7.5 Symbol.replace
對象的Symbol.replace屬性,指向一個方法,當該對象被String.prototype.replace方法調用時,會返回該方法的返回值。
String.prototype.replace(searchValue, replaceValue)
// 等同於
searchValue[Symbol.replace](this, replaceValue)
10.7.6 Symbol.search
對象的Symbol.search屬性,指向一個方法,當該對象被String.prototype.search方法調用時,會返回該方法的返回值。
String.prototype.search(regexp)
// 等同於
regexp[Symbol.search](this)
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
10.7.7 Symbol.split
對象的Symbol.split屬性,指向一個方法,當該對象被String.prototype.split方法調用時,會返回該方法的返回值。
String.prototype.split(separator, limit)
// 等同於
separator[Symbol.split](this, limit)
10.7.8 Symbol.iterator
對象的Symbol.iterator屬性,指向該對象的默認遍歷器方法。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
對象進行for...of循環時,會調用Symbol.iterator方法,返回該對象的默認遍歷器,詳細介紹參見《Iterator和for...of循環》一章。
class Collection {
*[Symbol.iterator]() {
let i = 0;
while(this[i] !== undefined) {
yield this[i];
++i;
}
}
}
let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(let value of myCollection) {
console.log(value);
}
// 1
// 2
10.7.9 Symbol.toPrimitive
對象的Symbol.toPrimitive屬性,指向一個方法。該對象被轉爲原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。
Symbol.toPrimitive被調用時,會接受一個字符串參數,表示當前運算的模式,一共有三種模式。
Number:該場合須要轉成數值
String:該場合須要轉成字符串
Default:該場合能夠轉成數值,也能夠轉成字符串
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
10.7.10 Symbol.toStringTag
對象的Symbol.toStringTag屬性,指向一個方法。在該對象上面調用Object.prototype.toString方法時,若是這個屬性存在,它的返回值會出現
在toString方法返回的字符串之中,表示對象的類型。也就是說,這個屬性能夠用來定製[object Object]或[object Array]中object後面的那個字符
串。
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
ES6新增內置對象的Symbol.toStringTag屬性值以下。
JSON[Symbol.toStringTag]:'JSON'
Math[Symbol.toStringTag]:'Math'
Module對象M[Symbol.toStringTag]:'Module'
ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer'
DataView.prototype[Symbol.toStringTag]:'DataView'
Map.prototype[Symbol.toStringTag]:'Map'
Promise.prototype[Symbol.toStringTag]:'Promise'
Set.prototype[Symbol.toStringTag]:'Set'
%TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array'等
WeakMap.prototype[Symbol.toStringTag]:'WeakMap'
WeakSet.prototype[Symbol.toStringTag]:'WeakSet'
%MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator'
%SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator'
%StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator'
Symbol.prototype[Symbol.toStringTag]:'Symbol'
Generator.prototype[Symbol.toStringTag]:'Generator'
GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'
10.7.10 Symbol.unscopables
對象的Symbol.unscopables屬性,指向一個對象。該對象指定了使用with關鍵字時,哪些屬性會被with環境排除。
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
上面代碼說明,數組有6個屬性,會被with命令排除。
// 沒有unscopables時
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有unscopables時
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
11 Proxy和Reflect
11.1 Proxy概述
Proxy用於修改某些操做的默認行爲,等同於在語言層面作出修改,因此屬於一種「元編程」(meta programming),即對編程語言進行編程。
Proxy能夠理解成,在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過
濾和改寫。Proxy這個詞的原意是代理,用在這裏表示由它來「代理」某些操做,能夠譯爲「代理器」。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
上面代碼對一個空對象架設了一層攔截,重定義了屬性的讀取(get)和設置(set)行爲。這裏暫時先不解釋具體的語法,只看運行結果。對設置了
攔截行爲的對象obj,去讀寫它的屬性,就會獲得下面的結果。
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上面代碼說明,Proxy實際上重載(overload)了點運算符,即用本身的定義覆蓋了語言的原始定義。
ES6原生提供Proxy構造函數,用來生成Proxy實例。
var proxy = new Proxy(target, handler);
Proxy對象的全部用法,都是上面這種形式,不一樣的只是handler參數的寫法。其中,new Proxy()表示生成一個Proxy實例,target參數表示所要攔截的
目標對象,handler參數也是一個對象,用來定製攔截行爲。
下面是另外一個攔截讀取屬性行爲的例子。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
上面代碼中,做爲構造函數,Proxy接受兩個參數。第一個參數是所要代理的目標對象(上例是一個空對象),即若是沒有Proxy的介入,操做原來要
訪問的就是這個對象;第二個參數是一個配置對象,對於每個被代理的操做,須要提供一個對應的處理函數,該函數將攔截對應的操做。好比,上
面代碼中,配置對象有一個get方法,用來攔截對目標對象屬性的訪問請求。get方法的兩個參數分別是目標對象和所要訪問的屬性。能夠看到,因爲
攔截函數老是返回35,因此訪問任何屬性都獲得35。
注意,要使得Proxy起做用,必須針對Proxy實例(上例是proxy對象)進行操做,而不是針對目標對象(上例是空對象)進行操做。
若是handler沒有設置任何攔截,那就等同於直接通向原對象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代碼中,handler是一個空對象,沒有任何攔截效果,訪問handeler就等同於訪問target。
一個技巧是將Proxy對象,設置到object.proxy屬性,從而能夠在object對象上調用。
var object = { proxy: new Proxy(target, handler) };
Proxy實例也能夠做爲其餘對象的原型對象。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代碼中,proxy對象是obj對象的原型,obj對象自己並無time屬性,因此根據原型鏈,會在proxy對象上讀取該屬性,致使被攔截。
同一個攔截器函數,能夠設置攔截多個操做。
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1,2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo // "Hello, foo"
下面是Proxy支持的攔截操做一覽。
對於能夠設置、但沒有設置攔截的操做,則直接落在目標對象上,按照原先的方式產生結果。
(1)get(target, propKey, receiver)
攔截對象屬性的讀取,好比proxy.foo和proxy['foo']。
最後一個參數receiver是一個對象,可選,參見下面Reflect.get的部分。
(2)set(target, propKey, value, receiver)
攔截對象屬性的設置,好比proxy.foo = v或proxy['foo'] = v,返回一個布爾值。
(3)has(target, propKey)
攔截propKey in proxy的操做,以及對象的hasOwnProperty方法,返回一個布爾值。
(4)deleteProperty(target, propKey)
攔截delete proxy[propKey]的操做,返回一個布爾值。
(5)ownKeys(target)
攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一個數組。該方法返回對象全部自
身的屬性,而Object.keys()僅返回對象可遍歷的屬性。
(6)getOwnPropertyDescriptor(target, propKey)
攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
(7)defineProperty(target, propKey, propDesc)
攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
(8)preventExtensions(target)
攔截Object.preventExtensions(proxy),返回一個布爾值。
(9)getPrototypeOf(target)
攔截Object.getPrototypeOf(proxy),返回一個對象。
(10)isExtensible(target)
攔截Object.isExtensible(proxy),返回一個布爾值。
(11)setPrototypeOf(target, proto)
攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。
若是目標對象是函數,那麼還有兩種額外操做能夠攔截。
(12)apply(target, object, args)
攔截Proxy實例做爲函數調用的操做,好比proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
(13)construct(target, args)
攔截Proxy實例做爲構造函數調用的操做,好比new proxy(...args)。
11.2 Proxy實例的方法
下面是上面這些攔截方法的詳細介紹。
11.2.1 get()
get方法用於攔截某個屬性的讀取操做。上文已經有一個例子,下面是另外一個攔截讀取操做的例子。
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 拋出一個錯誤
上面代碼表示,若是訪問目標對象不存在的屬性,會拋出一個錯誤。若是沒有這個攔截函數,訪問不存在的屬性,只會返回undefined。
get方法能夠繼承。
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.xxx // "GET xxx"
上面代碼中,攔截操做定義在Prototype對象上面,因此若是讀取obj對象繼承的屬性時,攔截會生效。
下面的例子使用get攔截,實現數組讀取負數的索引。
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c
上面代碼中,數組的位置參數是-1,就會輸出數組的倒數最後一個成員。
利用Proxy,能夠將讀取屬性的操做(get),轉變爲執行某個函數,從而實現屬性的鏈式操做。
var pipe = (function () {
return function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (pipeObject, fnName) {
if (fnName === 'get') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
}());
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
上面代碼設置Proxy之後,達到了將函數名鏈式使用的效果。
下面的例子則是利用get攔截,實現一個生成各類DOM節點的通用函數dom。
const dom = new Proxy({}, {
get(target, property) {
return function(attrs = {}, ...children) {
const el = document.createElement(property);
for (let prop of Object.keys(attrs)) {
el.setAttribute(prop, attrs[prop]);
}
for (let child of children) {
if (typeof child === 'string') {
child = document.createTextNode(child);
}
el.appendChild(child);
}
return el;
}
}
});
const el = dom.div({},
'Hello, my name is ',
dom.a({href: '//example.com'}, 'Mark'),
'. I like:',
dom.ul({},
dom.li({}, 'The web'),
dom.li({}, 'Food'),
dom.li({}, '…actually that\'s it')
)
);
document.body.appendChild(el);
11.2.2 set()
set方法用來攔截某個屬性的賦值操做。
假定Person對象有一個age屬性,該屬性應該是一個不大於200的整數,那麼能夠使用Proxy保證age的屬性值符合要求。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 對於age之外的屬性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯
上面代碼中,因爲設置了存值函數set,任何不符合要求的age屬性賦值,都會拋出一個錯誤。利用set方法,還能夠數據綁定,即每當對象發生變化
時,會自動更新DOM。
有時,咱們會在對象上面設置內部屬性,屬性名的第一個字符使用下劃線開頭,表示這些屬性不該該被外部使用。結合get和set方法,就能夠作到防
止這些內部屬性被外部讀寫。
var handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = {};
var proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
// Error: Invalid attempt to set private "_prop" property
上面代碼中,只要讀寫的屬性名的第一個字符是下劃線,一概拋錯,從而達到禁止讀寫內部屬性的目的。
11.2.3 apply()
apply方法攔截函數的調用、call和apply操做。
var handler = {
apply (target, ctx, args) {
return Reflect.apply(...arguments);
}
};
apply方法能夠接受三個參數,分別是目標對象、目標對象的上下文對象(this)和目標對象的參數數組。
下面是一個例子。
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
上面代碼中,變量p是Proxy的實例,當它做爲函數調用時(p()),就會被apply方法攔截,返回一個字符串。
下面是另一個例子。
var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
上面代碼中,每當執行proxy函數(直接調用或call和apply調用),就會被apply方法攔截。
另外,直接調用Reflect.apply方法,也會被攔截。
Reflect.apply(proxy, null, [9, 10]) // 38
11.2.4 has()
has方法用來攔截HasProperty操做,即判斷對象是否具備某個屬性時,這個方法會生效。典型的操做就是in運算符。
下面的例子使用has方法隱藏某些屬性,不被in運算符發現。
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
上面代碼中,若是原對象的屬性名的第一個字符是下劃線,proxy.has就會返回false,從而不會被in運算符發現。
若是原對象不可配置或者禁止擴展,這時has攔截會報錯。
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p // TypeError is thrown
上面代碼中,obj對象禁止擴展,結果使用has攔截就會報錯。
值得注意的是,has方法攔截的是HasProperty操做,而不是HasOwnProperty操做,即has方法不判斷一個屬性是對象自身的屬性,仍是繼承的屬性。
因爲for...in操做內部也會用到HasProperty操做,因此has方法在for...in循環時也會生效。
let stu1 = {name: 'Owen', score: 59};
let stu2 = {name: 'Mark', score: 99};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);
for (let a in oproxy1) {
console.log(oproxy1[a]);
}
// Owen
// Owen 不及格
for (let b in oproxy2) {
console.log(oproxy2[b]);
}
// Mark
// Mark 99
上面代碼中,for...in循環時,has攔截會生效,致使不符合要求的屬性被排除在for...in循環以外。
11.2.5 construct()
construct方法用於攔截new命令,下面是攔截對象的寫法。
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
construct方法能夠接受兩個參數。
target: 目標對象
args:構建函數的參數對象
下面是一個例子。
var p = new Proxy(function() {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
new p(1).value
// "called: 1"
// 10
construct方法返回的必須是一個對象,不然會報錯。
var p = new Proxy(function() {}, {
construct: function(target, argumentsList) {
return 1;
}
});
new p() // 報錯
11.2.6 deleteProperty()
deleteProperty方法用於攔截delete操做,若是這個方法拋出錯誤或者返回false,當前屬性就沒法被delete命令刪除。
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
上面代碼中,deleteProperty方法攔截了delete操做符,刪除第一個字符爲下劃線的屬性會報錯。
11.2.7 defineProperty()
defineProperty方法攔截了Object.defineProperty操做。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar'
// TypeError: proxy defineProperty handler returned false for property '"foo"'
上面代碼中,defineProperty方法返回false,致使添加新屬性會拋出錯誤。
11.2.8 getOwnPropertyDescriptor()
getOwnPropertyDescriptor方法攔截Object.getOwnPropertyDescriptor,返回一個屬性描述對象或者undefined。
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
上面代碼中,handler.getOwnPropertyDescriptor方法對於第一個字符爲下劃線的屬性名會返回undefined。
11.2.9 getPrototypeOf()
getPrototypeOf方法主要用來攔截Object.getPrototypeOf()運算符,以及其餘一些操做。
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof運算符
下面是一個例子。
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
上面代碼中,getPrototypeOf方法攔截Object.getPrototypeOf(),返回proto對象。
11.2.10 isExtensible()
isExtensible方法攔截Object.isExtensible操做。
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true
上面代碼設置了isExtensible方法,在調用Object.isExtensible時會輸出called。
這個方法有一個強限制,若是不能知足下面的條件,就會拋出錯誤。
Object.isExtensible(proxy) === Object.isExtensible(target)
下面是一個例子。
var p = new Proxy({}, {
isExtensible: function(target) {
return false;
}
});
Object.isExtensible(p) // 報錯
11.2.11 ownKeys()
ownKeys方法用來攔截Object.keys()操做。
let target = {};
let handler = {
ownKeys(target) {
return ['hello', 'world'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'hello', 'world' ]
上面代碼攔截了對於target對象的Object.keys()操做,返回預先設定的數組。
下面的例子是攔截第一個字符爲下劃線的屬性名。
let target = {
_bar: 'foo',
_prop: 'bar',
prop: 'baz'
};
let handler = {
ownKeys (target) {
return Reflect.ownKeys(target).filter(key => key[0] !== '_');
}
};
let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);
}
// "baz"
11.2.12 preventExtensions()
preventExtensions方法攔截Object.preventExtensions()。該方法必須返回一個布爾值。
這個方法有一個限制,只有當Object.isExtensible(proxy)爲false(即不可擴展)時,proxy.preventExtensions才能返回true,不然會報錯。
var p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p) // 報錯
上面代碼中,proxy.preventExtensions方法返回true,但這時Object.isExtensible(proxy)會返回true,所以報錯。
爲了防止出現這個問題,一般要在proxy.preventExtensions方法裏面,調用一次Object.preventExtensions。
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log("called");
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(p)
// "called"
// true
11.2.13 setPrototypeOf()
setPrototypeOf方法主要用來攔截Object.setPrototypeOf方法。
下面是一個例子。
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
proxy.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
上面代碼中,只要修改target的原型對象,就會報錯。
11.3 Proxy.revocable()
Proxy.revocable方法返回一個可取消的Proxy實例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable方法返回一個對象,該對象的proxy屬性是Proxy實例,revoke屬性是一個函數,能夠取消Proxy實例。上面代碼中,當執行revoke函
數以後,再訪問Proxy實例,就會拋出一個錯誤。
11.4 Reflect概述
Reflect對象與Proxy對象同樣,也是ES6爲了操做對象而提供的新API。Reflect對象的設計目的有這樣幾個。
(1) 將Object對象的一些明顯屬於語言內部的方法(好比Object.defineProperty),放到Reflect對象上。現階段,某些方法同時
在Object和Reflect對象上部署,將來的新方法將只部署在Reflect對象上。
(2) 修改某些Object方法的返回結果,讓其變得更合理。好比,Object.defineProperty(obj, name, desc)在沒法定義屬性時,會拋出一個錯誤,
而Reflect.defineProperty(obj, name, desc)則會返回false。
// 老寫法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3) 讓Object操做都變成函數行爲。某些Object操做是命令式,好比name in obj和delete obj[name],
而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行爲。
// 老寫法
'assign' in Object // true
// 新寫法
Reflect.has(Object, 'assign') // true
(4)Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象能夠方便
地調用對應的Reflect方法,完成默認行爲,做爲修改行爲的基礎。也就是說,無論Proxy怎麼修改默認行爲,你總能夠在Reflect上獲取默認行爲。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
上面代碼中,Proxy方法攔截target對象的屬性賦值行爲。它採用Reflect.set方法將值賦值給對象的屬性,而後再部署額外的功能。
下面是另外一個例子。
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
上面代碼中,每個Proxy對象的攔截操做(get、delete、has),內部都調用對應的Reflect方法,保證原生行爲可以正常執行。添加的工做,就是
將每個操做輸出一行日誌。
有了Reflect對象之後,不少操做會更易讀。
// 老寫法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新寫法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
11.5 Reflect對象的方法
Reflect對象的方法清單以下,共13個。
Reflect.apply(target,thisArg,args)
Reflect.construct(target,args)
Reflect.get(target,name,receiver)
Reflect.set(target,name,value,receiver)
Reflect.defineProperty(target,name,desc)
Reflect.deleteProperty(target,name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
上面這些方法的做用,大部分與Object對象的同名方法的做用都是相同的,並且它與Proxy對象的方法是一一對應的。下面是對其中幾個方法的解釋。
(1)Reflect.get(target, name, receiver)
查找並返回target對象的name屬性,若是沒有該屬性,則返回undefined。
若是name屬性部署了讀取函數,則讀取函數的this綁定receiver。
var obj = {
get foo() { return this.bar(); },
bar: function() { ... }
};
// 下面語句會讓 this.bar()
// 變成調用 wrapper.bar()
Reflect.get(obj, "foo", wrapper);
(2)Reflect.set(target, name, value, receiver)
設置target對象的name屬性等於value。若是name屬性設置了賦值函數,則賦值函數的this綁定receiver。
(3)Reflect.has(obj, name)
等同於name in obj。
(4)Reflect.deleteProperty(obj, name)
等同於delete obj[name]。
(5)Reflect.construct(target, args)
等同於new target(...args),這提供了一種不使用new,來調用構造函數的方法。
(6)Reflect.getPrototypeOf(obj)
讀取對象的__proto__屬性,對應Object.getPrototypeOf(obj)。
(7)Reflect.setPrototypeOf(obj, newProto)
設置對象的__proto__屬性,對應Object.setPrototypeOf(obj, newProto)。
(8)Reflect.apply(fun,thisArg,args)
等同於Function.prototype.apply.call(fun,thisArg,args)。通常來講,若是要綁定一個函數的this對象,能夠這樣寫fn.apply(obj, args),可是如
果函數定義了本身的apply方法,就只能寫成Function.prototype.apply.call(fn, obj, args),採用Reflect對象能夠簡化這種操做。
另外,須要注意的是,Reflect.set()、Reflect.defineProperty()、Reflect.freeze()、Reflect.seal()和Reflect.preventExtensions()返回一個
布爾值,表示操做是否成功。它們對應的Object方法,失敗時都會拋出錯誤。
// 失敗時拋出錯誤
Object.defineProperty(obj, name, desc);
// 失敗時返回false
Reflect.defineProperty(obj, name, desc);
上面代碼中,Reflect.defineProperty方法的做用與Object.defineProperty是同樣的,都是爲對象定義一個屬性。可是,Reflect.defineProperty方
法失敗時,不會拋出錯誤,只會返回false。
12 二進制數組
二進制數組(ArrayBuffer對象、TypedArray視圖和DataView視圖)是JavaScript操做二進制數據的一個接口。這些對象早就存在,屬於獨立的規格
(2011年2月發佈),ES6將它們歸入了ECMAScript規格,而且增長了新的方法。
這個接口的原始設計目的,與WebGL項目有關。所謂WebGL,就是指瀏覽器與顯卡之間的通訊接口,爲了知足JavaScript與顯卡之間大量的、實時的
數據交換,它們之間的數據通訊必須是二進制的,而不能是傳統的文本格式。文本格式傳遞一個32位整數,兩端的JavaScript腳本與顯卡都要進行格式
轉化,將很是耗時。這時要是存在一種機制,能夠像C語言那樣,直接操做字節,將4個字節的32位整數,以二進制形式原封不動地送入顯卡,腳本的
性能就會大幅提高。
二進制數組就是在這種背景下誕生的。它很像C語言的數組,容許開發者以數組下標的形式,直接操做內存,大大加強了JavaScript處理二進制數據的
能力,使得開發者有可能經過JavaScript與操做系統的原生接口進行二進制通訊。
二進制數組由三類對象組成。
(1)ArrayBuffer對象:表明內存之中的一段二進制數據,能夠經過「視圖」進行操做。「視圖」部署了數組接口,這意味着,能夠用數組的方法操做內
存。
(2)TypedArray視圖:共包括9種類型的視圖,好比Uint8Array(無符號8位整數)數組視圖, Int16Array(16位整數)數組視圖,
Float32Array(32位浮點數)數組視圖等等。
(3)DataView視圖:能夠自定義複合格式的視圖,好比第一個字節是Uint8(無符號8位整數)、第2、三個字節是Int16(16位整數)、第四個字節
開始是Float32(32位浮點數)等等,此外還能夠自定義字節序。
簡單說,ArrayBuffer對象表明原始的二進制數據,TypedArray視圖用來讀寫簡單類型的二進制數據,DataView視圖用來讀寫複雜類型的二進制數
據。
TypedArray視圖支持的數據類型一共有9種(DataView視圖支持除Uint8C之外的其餘8種)。
數據類型 字節長度 含義 對應的C語言類型
Int8 1 8位帶符號整數 signed char
Uint8 1 8位不帶符號整數 unsigned char
Uint8C 1 8位不帶符號整數(自動過濾溢出) unsigned char
Int16 2 16位帶符號整數 short
Uint16 2 16位不帶符號整數 unsigned short
Int32 4 32位帶符號整數 int
Uint32 4 32位不帶符號的整數 unsigned int
Float32 4 32位浮點數 float
Float64 8 64位浮點數 double
注意,二進制數組並非真正的數組,而是相似數組的對象。
不少瀏覽器操做的API,用到了二進制數組操做二進制數據,下面是其中的幾個。
File API
XMLHttpRequest
Fetch API
Canvas
WebSockets
12.1 ArrayBuffer對象
12.1.1 概述
ArrayBuffer對象表明儲存二進制數據的一段內存,它不能直接讀寫,只能經過視圖(TypedArray視圖和DataView視圖)來讀寫,視圖的做用是以指定
格式解讀二進制數據。
ArrayBuffer也是一個構造函數,能夠分配一段能夠存放數據的連續內存區域。
var buf = new ArrayBuffer(32);
上面代碼生成了一段32字節的內存區域,每一個字節的值默認都是0。能夠看到,ArrayBuffer構造函數的參數是所須要的內存大小(單位字節)。
爲了讀寫這段內容,須要爲它指定視圖。DataView視圖的建立,須要提供ArrayBuffer對象實例做爲參數。
var buf = new ArrayBuffer(32);
var dataView = new DataView(buf);
dataView.getUint8(0) // 0
上面代碼對一段32字節的內存,創建DataView視圖,而後以不帶符號的8位整數格式,讀取第一個元素,結果獲得0,由於原始內存的ArrayBuffer對
象,默認全部位都是0。
另外一種TypedArray視圖,與DataView視圖的一個區別是,它不是一個構造函數,而是一組構造函數,表明不一樣的數據格式。
var buffer = new ArrayBuffer(12);
var x1 = new Int32Array(buffer);
x1[0] = 1;
var x2 = new Uint8Array(buffer);
x2[0] = 2;
x1[0] // 2
上面代碼對同一段內存,分別創建兩種視圖:32位帶符號整數(Int32Array構造函數)和8位不帶符號整數(Uint8Array構造函數)。因爲兩個視圖
對應的是同一段內存,一個視圖修改底層內存,會影響到另外一個視圖。
TypedArray視圖的構造函數,除了接受ArrayBuffer實例做爲參數,還能夠接受普通數組做爲參數,直接分配內存生成底層的ArrayBuffer實例,並同
時完成對這段內存的賦值。
var typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3
typedArray[0] = 5;
typedArray // [5, 1, 2]
上面代碼使用TypedArray視圖的Uint8Array構造函數,新建一個不帶符號的8位整數視圖。能夠看到,Uint8Array直接使用普通數組做爲參數,對底
層內存的賦值同時完成。
12.1.2 ArrayBuffer.prototype.byteLength
ArrayBuffer實例的byteLength屬性,返回所分配的內存區域的字節長度。
var buffer = new ArrayBuffer(32);
buffer.byteLength
// 32
若是要分配的內存區域很大,有可能分配失敗(由於沒有那麼多的連續空餘內存),因此有必要檢查是否分配成功。
if (buffer.byteLength === n) {
// 成功
} else {
// 失敗
}
12.1.3 ArrayBuffer.prototype.slice()
ArrayBuffer實例有一個slice方法,容許將內存區域的一部分,拷貝生成一個新的ArrayBuffer對象。
var buffer = new ArrayBuffer(8);
var newBuffer = buffer.slice(0, 3);
上面代碼拷貝buffer對象的前3個字節(從0開始,到第3個字節前面結束),生成一個新的ArrayBuffer對象。slice方法其實包含兩步,第一步是先
分配一段新內存,第二步是將原來那個ArrayBuffer對象拷貝過去。
slice方法接受兩個參數,第一個參數表示拷貝開始的字節序號(含該字節),第二個參數表示拷貝截止的字節序號(不含該字節)。若是省略第二個
參數,則默認到原ArrayBuffer對象的結尾。
除了slice方法,ArrayBuffer對象不提供任何直接讀寫內存的方法,只容許在其上方創建視圖,而後經過視圖讀寫。
12.1.4 ArrayBuffer.isView()
ArrayBuffer有一個靜態方法isView,返回一個布爾值,表示參數是否爲ArrayBuffer的視圖實例。這個方法大體至關於判斷參數,是否爲TypedArray
實例或DataView實例。
var buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false
var v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
12.2 TypedArray視圖
12.2.1 概述
ArrayBuffer對象做爲內存區域,能夠存放多種類型的數據。同一段內存,不一樣數據有不一樣的解讀方式,這就叫作「視圖」(view)。ArrayBuffer有兩種
視圖,一種是TypedArray視圖,另外一種是DataView視圖。前者的數組成員都是同一個數據類型,後者的數組成員能夠是不一樣的數據類型。
目前,TypedArray視圖一共包括9種類型,每一種視圖都是一種構造函數。
Int8Array:8位有符號整數,長度1個字節。
Uint8Array:8位無符號整數,長度1個字節。
Uint8ClampedArray:8位無符號整數,長度1個字節,溢出處理不一樣。
Int16Array:16位有符號整數,長度2個字節。
Uint16Array:16位無符號整數,長度2個字節。
Int32Array:32位有符號整數,長度4個字節。
Uint32Array:32位無符號整數,長度4個字節。
Float32Array:32位浮點數,長度4個字節。
Float64Array:64位浮點數,長度8個字節。
這9個構造函數生成的數組,統稱爲TypedArray視圖。它們很像普通數組,都有length屬性,都能用方括號運算符([])獲取單個元素,全部數組的
方法,在它們上面都能使用。普通數組與TypedArray數組的差別主要在如下方面。
TypedArray數組的全部成員,都是同一種類型。
TypedArray數組的成員是連續的,不會有空位。
TypedArray數組成員的默認值爲0。好比,new Array(10)返回一個普通數組,裏面沒有任何成員,只是10個空位;new Uint8Array(10)返回一個
TypedArray數組,裏面10個成員都是0。
TypedArray數組只是一層視圖,自己不儲存數據,它的數據都儲存在底層的ArrayBuffer對象之中,要獲取底層對象必須使用buffer屬性。
12.2.2 構造函數
TypedArray數組提供9種構造函數,用來生成相應類型的數組實例。
構造函數有多種用法。
(1)TypedArray(buffer, byteOffset=0, length?)
同一個ArrayBuffer對象之上,能夠根據不一樣的數據類型,創建多個視圖。
// 建立一個8字節的ArrayBuffer
var b = new ArrayBuffer(8);
// 建立一個指向b的Int32視圖,開始於字節0,直到緩衝區的末尾
var v1 = new Int32Array(b);
// 建立一個指向b的Uint8視圖,開始於字節2,直到緩衝區的末尾
var v2 = new Uint8Array(b, 2);
// 建立一個指向b的Int16視圖,開始於字節2,長度爲2
var v3 = new Int16Array(b, 2, 2);
上面代碼在一段長度爲8個字節的內存(b)之上,生成了三個視圖:v一、v2和v3。
視圖的構造函數能夠接受三個參數:
第一個參數(必需):視圖對應的底層ArrayBuffer對象。
第二個參數(可選):視圖開始的字節序號,默認從0開始。
第三個參數(可選):視圖包含的數據個數,默認直到本段內存區域結束。
所以,v一、v2和v3是重疊的:v1[0]是一個32位整數,指向字節0~字節3;v2[0]是一個8位無符號整數,指向字節2;v3[0]是一個16位整數,指向字
節2~字節3。只要任何一個視圖對內存有所修改,就會在另外兩個視圖上反應出來。
注意,byteOffset必須與所要創建的數據類型一致,不然會報錯。
var buffer = new ArrayBuffer(8);
var i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2
上面代碼中,新生成一個8個字節的ArrayBuffer對象,而後在這個對象的第一個字節,創建帶符號的16位整數視圖,結果報錯。由於,帶符號的16位
整數須要兩個字節,因此byteOffset參數必須可以被2整除。
若是想從任意字節開始解讀ArrayBuffer對象,必須使用DataView視圖,由於TypedArray視圖只提供9種固定的解讀格式。
(2)TypedArray(length)
視圖還能夠不經過ArrayBuffer對象,直接分配內存而生成。
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];
上面代碼生成一個8個成員的Float64Array數組(共64字節),而後依次對每一個成員賦值。這時,視圖構造函數的參數就是成員的個數。能夠看到,視
圖數組的賦值操做與普通數組的操做毫無兩樣。
(3)TypedArray(typedArray)
TypedArray數組的構造函數,能夠接受另外一個TypedArray實例做爲參數。
var typedArray = new Int8Array(new Uint8Array(4));
上面代碼中,Int8Array構造函數接受一個Uint8Array實例做爲參數。
注意,此時生成的新數組,只是複製了參數數組的值,對應的底層內存是不同的。新數組會開闢一段新的內存儲存數據,不會在原數組的內存之上
創建視圖。
var x = new Int8Array([1, 1]);
var y = new Int8Array(x);
x[0] // 1
y[0] // 1
x[0] = 2;
y[0] // 1
上面代碼中,數組y是以數組x爲模板而生成的,當x變更的時候,y並無變更。
若是想基於同一段內存,構造不一樣的視圖,能夠採用下面的寫法。
var x = new Int8Array([1, 1]);
var y = new Int8Array(x.buffer);
x[0] // 1
y[0] // 1
x[0] = 2;
y[0] // 2
(4)TypedArray(arrayLikeObject)
構造函數的參數也能夠是一個普通數組,而後直接生成TypedArray實例。
var typedArray = new Uint8Array([1, 2, 3, 4]);
注意,這時TypedArray視圖會從新開闢內存,不會在原數組的內存上創建視圖。
上面代碼從一個普通的數組,生成一個8位無符號整數的TypedArray實例。
TypedArray數組也能夠轉換回普通數組。
var normalArray = Array.prototype.slice.call(typedArray);
12.2.3 數組方法
普通數組的操做方法和屬性,對TypedArray數組徹底適用。
TypedArray.prototype.copyWithin(target, start[, end = this.length])
TypedArray.prototype.entries()
TypedArray.prototype.every(callbackfn, thisArg?)
TypedArray.prototype.fill(value, start=0, end=this.length)
TypedArray.prototype.filter(callbackfn, thisArg?)
TypedArray.prototype.find(predicate, thisArg?)
TypedArray.prototype.findIndex(predicate, thisArg?)
TypedArray.prototype.forEach(callbackfn, thisArg?)
TypedArray.prototype.indexOf(searchElement, fromIndex=0)
TypedArray.prototype.join(separator)
TypedArray.prototype.keys()
TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
TypedArray.prototype.map(callbackfn, thisArg?)
TypedArray.prototype.reduce(callbackfn, initialValue?)
TypedArray.prototype.reduceRight(callbackfn, initialValue?)
TypedArray.prototype.reverse()
TypedArray.prototype.slice(start=0, end=this.length)
TypedArray.prototype.some(callbackfn, thisArg?)
TypedArray.prototype.sort(comparefn)
TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
TypedArray.prototype.toString()
TypedArray.prototype.values()
上面全部方法的用法,請參閱數組方法的介紹,這裏再也不重複了。
注意,TypedArray數組沒有concat方法。若是想要合併多個TypedArray數組,能夠用下面這個函數。
function concatenate(resultConstructor, ...arrays) {
let totalLength = 0;
for (let arr of arrays) {
totalLength += arr.length;
}
let result = new resultConstructor(totalLength);
let offset = 0;
for (let arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
concatenate(Uint8Array, Uint8Array.of(1, 2), Uint8Array.of(3, 4))
// Uint8Array [1, 2, 3, 4]
另外,TypedArray數組與普通數組同樣,部署了Iterator接口,因此能夠被遍歷。
let ui8 = Uint8Array.of(0, 1, 2);
for (let byte of ui8) {
console.log(byte);
}
// 0
// 1
// 2
12.2.4 字節序
字節序指的是數值在內存中的表示方式。
var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
上面代碼生成一個16字節的ArrayBuffer對象,而後在它的基礎上,創建了一個32位整數的視圖。因爲每一個32位整數佔據4個字節,因此一共能夠寫入
4個整數,依次爲0,2,4,6。
若是在這段數據上接着創建一個16位整數的視圖,則能夠讀出徹底不同的結果。
var int16View = new Int16Array(buffer);
for (var i = 0; i < int16View.length; i++) {
console.log("Entry " + i + ": " + int16View[i]);
}
// Entry 0: 0
// Entry 1: 0
// Entry 2: 2
// Entry 3: 0
// Entry 4: 4
// Entry 5: 0
// Entry 6: 6
// Entry 7: 0
因爲每一個16位整數佔據2個字節,因此整個ArrayBuffer對象如今分紅8段。而後,因爲x86體系的計算機都採用小端字節序(little endian),相對重要
的字節排在後面的內存地址,相對不重要字節排在前面的內存地址,因此就獲得了上面的結果。
好比,一個佔據四個字節的16進制數0x12345678,決定其大小的最重要的字節是「12」,最不重要的是「78」。小端字節序將最不重要的字節排在前面,
儲存順序就是78563412;大端字節序則徹底相反,將最重要的字節排在前面,儲存順序就是12345678。目前,全部我的電腦幾乎都是小端字節序,所
以TypedArray數組內部也採用小端字節序讀寫數據,或者更準確的說,按照本機操做系統設定的字節序讀寫數據。
這並不意味大端字節序不重要,事實上,不少網絡設備和特定的操做系統採用的是大端字節序。這就帶來一個嚴重的問題:若是一段數據是大端字節
序,TypedArray數組將沒法正確解析,由於它只能處理小端字節序!爲了解決這個問題,JavaScript引入DataView對象,能夠設定字節序,下文會詳
細介紹。
下面是另外一個例子。
// 假定某段buffer包含以下字節 [0x02, 0x01, 0x03, 0x07]
var buffer = new ArrayBuffer(4);
var v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;
var uInt16View = new Uint16Array(buffer);
// 計算機採用小端字節序
// 因此頭兩個字節等於258
if (uInt16View[0] === 258) {
console.log('OK'); // "OK"
}
// 賦值運算
uInt16View[0] = 255; // 字節變爲[0xFF, 0x00, 0x03, 0x07]
uInt16View[0] = 0xff05; // 字節變爲[0x05, 0xFF, 0x03, 0x07]
uInt16View[1] = 0x0210; // 字節變爲[0x05, 0xFF, 0x10, 0x02]
下面的函數能夠用來判斷,當前視圖是小端字節序,仍是大端字節序。
const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');
function getPlatformEndianness() {
let arr32 = Uint32Array.of(0x12345678);
let arr8 = new Uint8Array(arr32.buffer);
switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8[3])) {
case 0x12345678:
return BIG_ENDIAN;
case 0x78563412:
return LITTLE_ENDIAN;
default:
throw new Error('Unknown endianness');
}
}
總之,與普通數組相比,TypedArray數組的最大優勢就是能夠直接操做內存,不須要數據類型轉換,因此速度快得多。
12.2.5 BYTES_PER_ELEMENT屬性
每一種視圖的構造函數,都有一個BYTES_PER_ELEMENT屬性,表示這種數據類型佔據的字節數。
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8
這個屬性在TypedArray實例上也能獲取,即有TypedArray.prototype.BYTES_PER_ELEMENT。
12.2.6 ArrayBuffer與字符串的互相轉換
ArrayBuffer轉爲字符串,或者字符串轉爲ArrayBuffer,有一個前提,即字符串的編碼方法是肯定的。假定字符串採用UTF-16編碼(JavaScript的內
部編碼方式),能夠本身編寫轉換函數。
// ArrayBuffer轉爲字符串,參數爲ArrayBuffer對象
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
// 字符串轉爲ArrayBuffer對象,參數爲字符串
function str2ab(str) {
var buf = new ArrayBuffer(str.length * 2); // 每一個字符佔用2個字節
var bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
12.2.7 溢出
不一樣的視圖類型,所能容納的數值範圍是肯定的。超出這個範圍,就會出現溢出。好比,8位視圖只能容納一個8位的二進制值,若是放入一個9位的
值,就會溢出。
TypedArray數組的溢出處理規則,簡單來講,就是拋棄溢出的位,而後按照視圖類型進行解釋。
var uint8 = new Uint8Array(1);
uint8[0] = 256;
uint8[0] // 0
uint8[0] = -1;
uint8[0] // 255
上面代碼中,uint8是一個8位視圖,而256的二進制形式是一個9位的值100000000,這時就會發生溢出。根據規則,只會保留後8位,
即00000000。uint8視圖的解釋規則是無符號的8位整數,因此00000000就是0。
負數在計算機內部採用「2的補碼」表示,也就是說,將對應的正數值進行否運算,而後加1。好比,-1對應的正值是1,進行否運算之後,得
到11111110,再加上1就是補碼形式11111111。uint8按照無符號的8位整數解釋11111111,返回結果就是255。
一個簡單轉換規則,能夠這樣表示。
正向溢出(overflow):當輸入值大於當前數據類型的最大值,結果等於當前數據類型的最小值加上餘值,再減去1。
負向溢出(underflow):當輸入值小於當前數據類型的最小值,結果等於當前數據類型的最大值減去餘值,再加上1。
請看下面的例子。
var int8 = new Int8Array(1);
int8[0] = 128;
int8[0] // -128
int8[0] = -129;
int8[0] // 127
上面例子中,int8是一個帶符號的8位整數視圖,它的最大值是127,最小值是-128。輸入值爲128時,至關於正向溢出1,根據「最小值加上餘值,再
減去1」的規則,就會返回-128;輸入值爲-129時,至關於負向溢出1,根據「最大值減去餘值,再加上1」的規則,就會返回127。
Uint8ClampedArray視圖的溢出規則,與上面的規則不一樣。它規定,凡是發生正向溢出,該值一概等於當前數據類型的最大值,即255;若是發生負向
溢出,該值一概等於當前數據類型的最小值,即0。
var uint8c = new Uint8ClampedArray(1);
uint8c[0] = 256;
uint8c[0] // 255
uint8c[0] = -1;
uint8c[0] // 0
上面例子中,uint8C是一個Uint8ClampedArray視圖,正向溢出時都返回255,負向溢出都返回0。
12.2.8 TypedArray.prototype.buffer
TypedArray實例的buffer屬性,返回整段內存區域對應的ArrayBuffer對象。該屬性爲只讀屬性。
var a = new Float32Array(64);
var b = new Uint8Array(a.buffer);
上面代碼的a視圖對象和b視圖對象,對應同一個ArrayBuffer對象,即同一段內存。
12.2.9 TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset
byteLength屬性返回TypedArray數組佔據的內存長度,單位爲字節。byteOffset屬性返回TypedArray數組從底層ArrayBuffer對象的哪一個字節開始。
這兩個屬性都是隻讀屬性。
var b = new ArrayBuffer(8);
var v1 = new Int32Array(b);
var v2 = new Uint8Array(b, 2);
var v3 = new Int16Array(b, 2, 2);
v1.byteLength // 8
v2.byteLength // 6
v3.byteLength // 4
v1.byteOffset // 0
v2.byteOffset // 2
v3.byteOffset // 2
12.2.10 TypedArray.prototype.length
length屬性表示TypedArray數組含有多少個成員。注意將byteLength屬性和length屬性區分,前者是字節長度,後者是成員長度。
var a = new Int16Array(8);
a.length // 8
a.byteLength // 16
12.2.11 TypedArray.prototype.set()
TypedArray數組的set方法用於複製數組(普通數組或TypedArray數組),也就是將一段內容徹底複製到另外一段內存。
var a = new Uint8Array(8);
var b = new Uint8Array(8);
b.set(a);
上面代碼複製a數組的內容到b數組,它是整段內存的複製,比一個個拷貝成員的那種複製快得多。
set方法還能夠接受第二個參數,表示從b對象的哪個成員開始複製a對象。
var a = new Uint16Array(8);
var b = new Uint16Array(10);
b.set(a, 2)
上面代碼的b數組比a數組多兩個成員,因此從b[2]開始複製。
12.2.12 TypedArray.prototype.subarray()
subarray方法是對於TypedArray數組的一部分,再創建一個新的視圖。
var a = new Uint16Array(8);
var b = a.subarray(2,3);
a.byteLength // 16
b.byteLength // 2
subarray方法的第一個參數是起始的成員序號,第二個參數是結束的成員序號(不含該成員),若是省略則包含剩餘的所有成員。因此,上面代碼
的a.subarray(2,3),意味着b只包含a[2]一個成員,字節長度爲2。
12.2.13 TypedArray.prototype.slice()
TypeArray實例的slice方法,能夠返回一個指定位置的新的TypedArray實例。
let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1)
// Uint8Array [ 2 ]
上面代碼中,ui8是8位無符號整數數組視圖的一個實例。它的slice方法能夠從當前視圖之中,返回一個新的視圖實例。
slice方法的參數,表示原數組的具體位置,開始生成新數組。負值表示逆向的位置,即-1爲倒數第一個位置,-2表示倒數第二個位置,以此類推。
12.2.14 TypedArray.of()
TypedArray數組的全部構造函數,都有一個靜態方法of,用於將參數轉爲一個TypedArray實例。
Float32Array.of(0.151, -8, 3.7)
// Float32Array [ 0.151, -8, 3.7 ]
下面三種方法都會生成一樣一個TypedArray數組。
// 方法一
let tarr = new Uint8Array([1,2,3]);
// 方法二
let tarr = Uint8Array.of(1,2,3);
// 方法三
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;
12.2.15 TypedArray.from()
靜態方法from接受一個可遍歷的數據結構(好比數組)做爲參數,返回一個基於這個結構的TypedArray實例。
Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]
這個方法還能夠將一種TypedArray實例,轉爲另外一種。
var ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array // true
from方法還能夠接受一個函數,做爲第二個參數,用來對每一個元素進行遍歷,功能相似map方法。
Int8Array.of(127, 126, 125).map(x => 2 * x)
// Int8Array [ -2, -4, -6 ]
Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
// Int16Array [ 254, 252, 250 ]
上面的例子中,from方法沒有發生溢出,這說明遍歷不是針對原來的8位整數數組。也就是說,from會將第一個參數指定的TypedArray數組,拷貝到
另外一段內存之中,處理以後再將結果轉成指定的數組格式。
12.3 複合視圖
因爲視圖的構造函數能夠指定起始位置和長度,因此在同一段內存之中,能夠依次存放不一樣類型的數據,這叫作「複合視圖」。
var buffer = new ArrayBuffer(24);
var idView = new Uint32Array(buffer, 0, 1);
var usernameView = new Uint8Array(buffer, 4, 16);
var amountDueView = new Float32Array(buffer, 20, 1);
上面代碼將一個24字節長度的ArrayBuffer對象,分紅三個部分:
字節0到字節3:1個32位無符號整數
字節4到字節19:16個8位整數
字節20到字節23:1個32位浮點數
這種數據結構能夠用以下的C語言描述:
struct someStruct {
unsigned long id;
char username[16];
float amountDue;
};
12.4 DataView視圖
若是一段數據包括多種類型(好比服務器傳來的HTTP數據),這時除了創建ArrayBuffer對象的複合視圖之外,還能夠經過DataView視圖進行操做。
DataView視圖提供更多操做選項,並且支持設定字節序。原本,在設計目的上,ArrayBuffer對象的各類TypedArray視圖,是用來向網卡、聲卡之類
的本機設備傳送數據,因此使用本機的字節序就能夠了;而DataView視圖的設計目的,是用來處理網絡設備傳來的數據,因此大端字節序或小端字節
序是能夠自行設定的。
DataView視圖自己也是構造函數,接受一個ArrayBuffer對象做爲參數,生成視圖。
DataView(ArrayBuffer buffer [, 字節起始位置 [, 長度]]);
下面是一個例子。
var buffer = new ArrayBuffer(24);
var dv = new DataView(buffer);
DataView實例有如下屬性,含義與TypedArray實例的同名方法相同。
DataView.prototype.buffer:返回對應的ArrayBuffer對象
DataView.prototype.byteLength:返回佔據的內存字節長度
DataView.prototype.byteOffset:返回當前視圖從對應的ArrayBuffer對象的哪一個字節開始
DataView實例提供8個方法讀取內存。
getInt8:讀取1個字節,返回一個8位整數。
getUint8:讀取1個字節,返回一個無符號的8位整數。
getInt16:讀取2個字節,返回一個16位整數。
getUint16:讀取2個字節,返回一個無符號的16位整數。
getInt32:讀取4個字節,返回一個32位整數。
getUint32:讀取4個字節,返回一個無符號的32位整數。
getFloat32:讀取4個字節,返回一個32位浮點數。
getFloat64:讀取8個字節,返回一個64位浮點數。
這一系列get方法的參數都是一個字節序號(不能是負數,不然會報錯),表示從哪一個字節開始讀取。
var buffer = new ArrayBuffer(24);
var dv = new DataView(buffer);
// 從第1個字節讀取一個8位無符號整數
var v1 = dv.getUint8(0);
// 從第2個字節讀取一個16位無符號整數
var v2 = dv.getUint16(1);
// 從第4個字節讀取一個16位無符號整數
var v3 = dv.getUint16(3);
上面代碼讀取了ArrayBuffer對象的前5個字節,其中有一個8位整數和兩個十六位整數。
若是一次讀取兩個或兩個以上字節,就必須明確數據的存儲方式,究竟是小端字節序仍是大端字節序。默認狀況下,DataView的get方法使用大端字節
序解讀數據,若是須要使用小端字節序解讀,必須在get方法的第二個參數指定true。
// 小端字節序
var v1 = dv.getUint16(1, true);
// 大端字節序
var v2 = dv.getUint16(3, false);
// 大端字節序
var v3 = dv.getUint16(3);
DataView視圖提供8個方法寫入內存。
setInt8:寫入1個字節的8位整數。
setUint8:寫入1個字節的8位無符號整數。
setInt16:寫入2個字節的16位整數。
setUint16:寫入2個字節的16位無符號整數。
setInt32:寫入4個字節的32位整數。
setUint32:寫入4個字節的32位無符號整數。
setFloat32:寫入4個字節的32位浮點數。
setFloat64:寫入8個字節的64位浮點數。
這一系列set方法,接受兩個參數,第一個參數是字節序號,表示從哪一個字節開始寫入,第二個參數爲寫入的數據。對於那些寫入兩個或兩個以上字節
的方法,須要指定第三個參數,false或者undefined表示使用大端字節序寫入,true表示使用小端字節序寫入。
// 在第1個字節,以大端字節序寫入值爲25的32位整數
dv.setInt32(0, 25, false);
// 在第5個字節,以大端字節序寫入值爲25的32位整數
dv.setInt32(4, 25);
// 在第9個字節,以小端字節序寫入值爲2.5的32位浮點數
dv.setFloat32(8, 2.5, true);
若是不肯定正在使用的計算機的字節序,能夠採用下面的判斷方式。
var littleEndian = (function() {
var buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true);
return new Int16Array(buffer)[0] === 256;
})();
若是返回true,就是小端字節序;若是返回false,就是大端字節序。
12.5 二進制數組的應用
大量的Web API用到了ArrayBuffer對象和它的視圖對象。
12.5.1 AJAX
傳統上,服務器經過AJAX操做只能返回文本數據,即responseType屬性默認爲text。XMLHttpRequest第二版XHR2容許服務器返回二進制數據,這時分
成兩種狀況。若是明確知道返回的二進制數據類型,能夠把返回類型(responseType)設爲arraybuffer;若是不知道,就設爲blob。
var xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
let arrayBuffer = xhr.response;
// ···
};
xhr.send();
若是知道傳回來的是32位整數,能夠像下面這樣處理。
xhr.onreadystatechange = function () {
if (req.readyState === 4 ) {
var arrayResponse = xhr.response;
var dataView = new DataView(arrayResponse);
var ints = new Uint32Array(dataView.byteLength / 4);
xhrDiv.style.backgroundColor = "#00FF00";
xhrDiv.innerText = "Array is " + ints.length + "uints long";
}
}
12.5.2 Canvas
網頁Canvas元素輸出的二進制像素數據,就是TypedArray數組。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var uint8ClampedArray = imageData.data;
須要注意的是,上面代碼的uint8ClampedArray雖然是一個TypedArray數組,可是它的視圖類型是一種針對Canvas元素的專有類
型Uint8ClampedArray。這個視圖類型的特色,就是專門針對顏色,把每一個字節解讀爲無符號的8位整數,即只能取值0~255,並且發生運算的時候自
動過濾高位溢出。這爲圖像處理帶來了巨大的方便。
舉例來講,若是把像素的顏色值設爲Uint8Array類型,那麼乘以一個gamma值的時候,就必須這樣計算:
u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));
由於Uint8Array類型對於大於255的運算結果(好比0xFF+1),會自動變爲0x00,因此圖像處理必需要像上面這樣算。這樣作很麻煩,並且影響性
能。若是將顏色值設爲Uint8ClampedArray類型,計算就簡化許多。
pixels[i] *= gamma;
Uint8ClampedArray類型確保將小於0的值設爲0,將大於255的值設爲255。注意,IE 10不支持該類型。
12.5.3 WebSocket
WebSocket能夠經過ArrayBuffer,發送或接收二進制數據。
var socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';
// Wait until socket is open
socket.addEventListener('open', function (event) {
// Send binary data
var typedArray = new Uint8Array(4);
socket.send(typedArray.buffer);
});
// Receive binary data
socket.addEventListener('message', function (event) {
var arrayBuffer = event.data;
// ···
});
12.5.4 Fetch API
Fetch API取回的數據,就是ArrayBuffer對象。
fetch(url)
.then(function(request){
return request.arrayBuffer()
})
.then(function(arrayBuffer){
// ...
});
12.5.5 File API
若是知道一個文件的二進制數據類型,也能夠將這個文件讀取爲ArrayBuffer對象。
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
var arrayBuffer = reader.result;
// ···
};
下面以處理bmp文件爲例。假定file變量是一個指向bmp文件的文件對象,首先讀取文件。
var reader = new FileReader();
reader.addEventListener("load", processimage, false);
reader.readAsArrayBuffer(file);
而後,定義處理圖像的回調函數:先在二進制數據之上創建一個DataView視圖,再創建一個bitmap對象,用於存放處理後的數據,最後將圖像展現
在Canvas元素之中。
function processimage(e) {
var buffer = e.target.result;
var datav = new DataView(buffer);
var bitmap = {};
// 具體的處理步驟
}
具體處理圖像數據時,先處理bmp的文件頭。具體每一個文件頭的格式和定義,請參閱有關資料。
bitmap.fileheader = {};
bitmap.fileheader.bfType = datav.getUint16(0, true);
bitmap.fileheader.bfSize = datav.getUint32(2, true);
bitmap.fileheader.bfReserved1 = datav.getUint16(6, true);
bitmap.fileheader.bfReserved2 = datav.getUint16(8, true);
bitmap.fileheader.bfOffBits = datav.getUint32(10, true);
接着處理圖像元信息部分。
bitmap.infoheader = {};
bitmap.infoheader.biSize = datav.getUint32(14, true);
bitmap.infoheader.biWidth = datav.getUint32(18, true);
bitmap.infoheader.biHeight = datav.getUint32(22, true);
bitmap.infoheader.biPlanes = datav.getUint16(26, true);
bitmap.infoheader.biBitCount = datav.getUint16(28, true);
bitmap.infoheader.biCompression = datav.getUint32(30, true);
bitmap.infoheader.biSizeImage = datav.getUint32(34, true);
bitmap.infoheader.biXPelsPerMeter = datav.getUint32(38, true);
bitmap.infoheader.biYPelsPerMeter = datav.getUint32(42, true);
bitmap.infoheader.biClrUsed = datav.getUint32(46, true);
bitmap.infoheader.biClrImportant = datav.getUint32(50, true);
最後處理圖像自己的像素信息。
var start = bitmap.fileheader.bfOffBits;
bitmap.pixels = new Uint8Array(buffer, start);
至此,圖像文件的數據所有處理完成。下一步,能夠根據須要,進行圖像變形,或者轉換格式,或者展現在Canvas網頁元素之中。
13 Set和Map數據結構
13.1 Set
13.1.1 基本用法
ES6提供了新的數據結構Set。它相似於數組,可是成員的值都是惟一的,沒有重複的值。
Set自己是一個構造函數,用來生成Set數據結構。
var s = new Set();
[2, 3, 5, 4, 5, 2, 2].map(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
上面代碼經過add方法向Set結構加入成員,結果代表Set結構不會添加劇復的值。
Set函數能夠接受一個數組(或相似數組的對象)做爲參數,用來初始化。
// 例一
var set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
var items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
function divs () {
return [...document.querySelectorAll('div')];
}
var set = new Set(divs());
set.size // 56
// 相似於
divs().forEach(div => set.add(div));
set.size // 56
上面代碼中,例一和例二都是Set函數接受數組做爲參數,例三是接受相似數組的對象做爲參數。
上面代碼中,也展現了一種去除數組重複成員的方法。
// 去除數組的重複成員
[...new Set(array)]
向Set加入值的時候,不會發生類型轉換,因此5和"5"是兩個不一樣的值。Set內部判斷兩個值是否不一樣,使用的算法叫作「Same-value equality」,它類
似於精確相等運算符(===),主要的區別是NaN等於自身,而精確相等運算符認爲NaN不等於自身。
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
上面代碼向Set實例添加了兩個NaN,可是隻能加入一個。這代表,在Set內部,兩個NaN是相等。
另外,兩個對象老是不相等的。
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
上面代碼表示,因爲兩個空對象不相等,因此它們被視爲兩個值。
13.1.2 Set實例的屬性和方法
Set結構的實例有如下屬性。
Set.prototype.constructor:構造函數,默認就是Set函數。
Set.prototype.size:返回Set實例的成員總數。
Set實例的方法分爲兩大類:操做方法(用於操做數據)和遍歷方法(用於遍歷成員)。下面先介紹四個操做方法。
add(value):添加某個值,返回Set結構自己。
delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功。
has(value):返回一個布爾值,表示該值是否爲Set的成員。
clear():清除全部成員,沒有返回值。
上面這些屬性和方法的實例以下。
s.add(1).add(2).add(2);
// 注意2被加入了兩次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
下面是一個對比,看看在判斷是否包括一個鍵上面,Object結構和Set結構的寫法不一樣。
// 對象的寫法
var properties = {
'width': 1,
'height': 1
};
if (properties[someName]) {
// do something
}
// Set的寫法
var properties = new Set();
properties.add('width');
properties.add('height');
if (properties.has(someName)) {
// do something
}
Array.from方法能夠將Set結構轉爲數組。
var items = new Set([1, 2, 3, 4, 5]);
var array = Array.from(items);
這就提供了去除數組重複成員的另外一種方法。
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
13.1.3 遍歷操做
Set結構的實例有四個遍歷方法,能夠用於遍歷成員。
keys():返回鍵名的遍歷器
values():返回鍵值的遍歷器
entries():返回鍵值對的遍歷器
forEach():使用回調函數遍歷每一個成員
須要特別指出的是,Set的遍歷順序就是插入順序。這個特性有時很是有用,好比使用Set保存一個回調函數列表,調用時就能保證按照添加順序調
用。
(1)keys(),values(),entries()
key方法、value方法、entries方法返回的都是遍歷器對象(詳見《Iterator對象》一章)。因爲Set結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同
一個值),因此key方法和value方法的行爲徹底一致。
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
上面代碼中,entries方法返回的遍歷器,同時包括鍵名和鍵值,因此每次輸出一個數組,它的兩個成員徹底相等。
Set結構的實例默承認遍歷,它的默認遍歷器生成函數就是它的values方法。
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
這意味着,能夠省略values方法,直接用for...of循環遍歷Set。
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red
// green
// blue
(2)forEach()
Set結構的實例的forEach方法,用於對每一個成員執行某種操做,沒有返回值。
let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(value * 2) )
// 2
// 4
// 6
上面代碼說明,forEach方法的參數就是一個處理函數。該函數的參數依次爲鍵值、鍵名、集合自己(上例省略了該參數)。另外,forEach方法還可
以有第二個參數,表示綁定的this對象。
(3)遍歷的應用
擴展運算符(...)內部使用for...of循環,因此也能夠用於Set結構。
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']
擴展運算符和Set結構相結合,就能夠去除數組的重複成員。
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
並且,數組的map和filter方法也能夠用於Set了。
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set結構:{2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set結構:{2, 4}
所以使用Set能夠很容易地實現並集(Union)、交集(Intersect)和差集(Difference)。
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 並集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
若是想在遍歷操做中,同步改變原來的Set結構,目前沒有直接的方法,但有兩種變通方法。一種是利用原Set結構映射出一個新的結構,而後賦值給
原來的Set結構;另外一種是利用Array.from方法。
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6
// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
上面代碼提供了兩種方法,直接在遍歷操做中改變原來的Set結構。
13.2 WeakSet
WeakSet結構與Set相似,也是不重複的值的集合。可是,它與Set有兩個區別。
首先,WeakSet的成員只能是對象,而不能是其餘類型的值。
其次,WeakSet中的對象都是弱引用,即垃圾回收機制不考慮WeakSet對該對象的引用,也就是說,若是其餘對象都再也不引用該對象,那麼垃圾回收
機制會自動回收該對象所佔用的內存,不考慮該對象還存在於WeakSet之中。這個特色意味着,沒法引用WeakSet的成員,所以WeakSet是不可遍歷
的。
var ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
上面代碼試圖向WeakSet添加一個數值和Symbol值,結果報錯,由於WeakSet只能放置對象。
WeakSet是一個構造函數,能夠使用new命令,建立WeakSet數據結構。
var ws = new WeakSet();
做爲構造函數,WeakSet能夠接受一個數組或相似數組的對象做爲參數。(實際上,任何具備iterable接口的對象,均可以做爲WeakSet的參數。)該
數組的全部成員,都會自動成爲WeakSet實例對象的成員。
var a = [[1,2], [3,4]];
var ws = new WeakSet(a);
上面代碼中,a是一個數組,它有兩個成員,也都是數組。將a做爲WeakSet構造函數的參數,a的成員會自動成爲WeakSet的成員。
注意,是a數組的成員成爲WeakSet的成員,而不是a數組自己。這意味着,數組的成員只能是對象。
var b = [3, 4];
var ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
上面代碼中,數組b的成員不是對象,加入WeaKSet就會報錯。
WeakSet結構有如下三個方法。
WeakSet.prototype.add(value):向WeakSet實例添加一個新成員。
WeakSet.prototype.delete(value):清除WeakSet實例的指定成員。
WeakSet.prototype.has(value):返回一個布爾值,表示某個值是否在WeakSet實例之中。
下面是一個例子。
var ws = new WeakSet();
var obj = {};
var foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false
ws.delete(window);
ws.has(window); // false
WeakSet沒有size屬性,沒有辦法遍歷它的成員。
ws.size // undefined
ws.forEach // undefined
ws.forEach(function(item){ console.log('WeakSet has ' + item)})
// TypeError: undefined is not a function
上面代碼試圖獲取size和forEach屬性,結果都不能成功。
WeakSet不能遍歷,是由於成員都是弱引用,隨時可能消失,遍歷機制沒法保證成員的存在,極可能剛剛遍歷結束,成員就取不到了。WeakSet的一
個用處,是儲存DOM節點,而不用擔憂這些節點從文檔移除時,會引起內存泄漏。
下面是WeakSet的另外一個例子。
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的實例上調用!');
}
}
}
上面代碼保證了Foo的實例方法,只能在Foo的實例上調用。這裏使用WeakSet的好處是,foos對實例的引用,不會被計入內存回收機制,因此刪除實
例的時候,不用考慮foos,也不會出現內存泄漏。
13.3 Map
13.3.1 Map結構的目的和基本用法
JavaScript的對象(Object),本質上是鍵值對的集合(Hash結構),可是傳統上只能用字符串看成鍵。這給它的使用帶來了很大的限制。
var data = {};
var element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
上面代碼原意是將一個DOM節點做爲對象data的鍵,可是因爲對象只接受字符串做爲鍵名,因此element被自動轉爲字符
串[object HTMLDivElement]。
爲了解決這個問題,ES6提供了Map數據結構。它相似於對象,也是鍵值對的集合,可是「鍵」的範圍不限於字符串,各類類型的值(包括對象)均可以
看成鍵。也就是說,Object結構提供了「字符串—值」的對應,Map結構提供了「值—值」的對應,是一種更完善的Hash結構實現。若是你須要「鍵值對」的
數據結構,Map比Object更合適。
var m = new Map();
var o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
上面代碼使用set方法,將對象o看成m的一個鍵,而後又使用get方法讀取這個鍵,接着使用delete方法刪除了這個鍵。
做爲構造函數,Map也能夠接受一個數組做爲參數。該數組的成員是一個個表示鍵值對的數組。
var map = new Map([
['name', '張三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"
上面代碼在新建Map實例時,就指定了兩個鍵name和title。
Map構造函數接受數組做爲參數,實際上執行的是下面的算法。
var items = [
['name', '張三'],
['title', 'Author']
];
var map = new Map();
items.forEach(([key, value]) => map.set(key, value));
下面的例子中,字符串true和布爾值true是兩個不一樣的鍵。
var m = new Map([
[true, 'foo'],
['true', 'bar']
]);
m.get(true) // 'foo'
m.get('true') // 'bar'
若是對同一個鍵屢次賦值,後面的值將覆蓋前面的值。
let map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"
上面代碼對鍵1連續賦值兩次,後一次的值覆蓋前一次的值。
若是讀取一個未知的鍵,則返回undefined。
new Map().get('asfddfsasadf')
// undefined
注意,只有對同一個對象的引用,Map結構纔將其視爲同一個鍵。這一點要很是當心。
var map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代碼的set和get方法,表面是針對同一個鍵,但實際上這是兩個值,內存地址是不同的,所以get方法沒法讀取該鍵,返回undefined。
同理,一樣的值的兩個實例,在Map結構中被視爲兩個鍵。
var map = new Map();
var k1 = ['a'];
var k2 = ['a'];
map
.set(k1, 111)
.set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
上面代碼中,變量k1和k2的值是同樣的,可是它們在Map結構中被視爲兩個鍵。
由上可知,Map的鍵其實是跟內存地址綁定的,只要內存地址不同,就視爲兩個鍵。這就解決了同名屬性碰撞(clash)的問題,咱們擴展別人的
庫的時候,若是使用對象做爲鍵名,就不用擔憂本身的屬性與原做者的屬性同名。
若是Map的鍵是一個簡單類型的值(數字、字符串、布爾值),則只要兩個值嚴格相等,Map將其視爲一個鍵,包括0和-0。另外,雖然NaN不嚴格相
等於自身,但Map將其視爲同一個鍵。
let map = new Map();
map.set(NaN, 123);
map.get(NaN) // 123
map.set(-0, 123);
map.get(+0) // 123
13.3.2 實例的屬性和操做方法
Map結構的實例有如下屬性和操做方法。
(1)size屬性
size屬性返回Map結構的成員總數。
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
(2)set(key, value)
set方法設置key所對應的鍵值,而後返回整個Map結構。若是key已經有值,則鍵值會被更新,不然就新生成該鍵。
var m = new Map();
m.set("edition", 6) // 鍵是字符串
m.set(262, "standard") // 鍵是數值
m.set(undefined, "nah") // 鍵是undefined
set方法返回的是Map自己,所以能夠採用鏈式寫法。
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
(3)get(key)
get方法讀取key對應的鍵值,若是找不到key,返回undefined。
var m = new Map();
var hello = function() {console.log("hello");}
m.set(hello, "Hello ES6!") // 鍵是函數
m.get(hello) // Hello ES6!
(4)has(key)
has方法返回一個布爾值,表示某個鍵是否在Map數據結構中。
var m = new Map();
m.set("edition", 6);
m.set(262, "standard");
m.set(undefined, "nah");
m.has("edition") // true
m.has("years") // false
m.has(262) // true
m.has(undefined) // true
(5)delete(key)
delete方法刪除某個鍵,返回true。若是刪除失敗,返回false。
var m = new Map();
m.set(undefined, "nah");
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
(6)clear()
clear方法清除全部成員,沒有返回值。
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0
13.3.3 遍歷方法
Map原生提供三個遍歷器生成函數和一個遍歷方法。
keys():返回鍵名的遍歷器。
values():返回鍵值的遍歷器。
entries():返回全部成員的遍歷器。
forEach():遍歷Map的全部成員。
須要特別注意的是,Map的遍歷順序就是插入順序。
下面是使用實例。
let map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// 等同於使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
上面代碼最後的那個例子,表示Map結構的默認遍歷器接口(Symbol.iterator屬性),就是entries方法。
map[Symbol.iterator] === map.entries
// true
Map結構轉爲數組結構,比較快速的方法是結合使用擴展運算符(...)。
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
結合數組的map方法、filter方法,能夠實現Map的遍歷和過濾(Map自己沒有map和filter方法)。
let map0 = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
let map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
// 產生Map結構 {1 => 'a', 2 => 'b'}
let map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 產生Map結構 {2 => '_a', 4 => '_b', 6 => '_c'}
此外,Map還有一個forEach方法,與數組的forEach方法相似,也能夠實現遍歷。
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
forEach方法還能夠接受第二個參數,用來綁定this。
var reporter = {
report: function(key, value) {
console.log("Key: %s, Value: %s", key, value);
}
};
map.forEach(function(value, key, map) {
this.report(key, value);
}, reporter);
上面代碼中,forEach方法的回調函數的this,就指向reporter。
13.3.4 與其餘數據結構的互相轉換
(1)Map轉爲數組
前面已經提過,Map轉爲數組最方便的方法,就是使用擴展運算符(...)。
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)數組轉爲Map
將數組轉入Map構造函數,就能夠轉爲Map。
new Map([[true, 7], [{foo: 3}, ['abc']]])
// Map {true => 7, Object {foo: 3} => ['abc']}
(3)Map轉爲對象
若是全部Map的鍵都是字符串,它能夠轉爲對象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
(4)對象轉爲Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// [ [ 'yes', true ], [ 'no', false ] ]
(5)Map轉爲JSON
Map轉爲JSON要區分兩種狀況。一種狀況是,Map的鍵名都是字符串,這時能夠選擇轉爲對象JSON。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另外一種狀況是,Map的鍵名有非字符串,這時能夠選擇轉爲數組JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
(6)JSON轉爲Map
JSON轉爲Map,正常狀況下,全部鍵名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes":true,"no":false}')
// Map {'yes' => true, 'no' => false}
可是,有一種特殊狀況,整個JSON就是一個數組,且每一個數組成員自己,又是一個有兩個成員的數組。這時,它能夠一一對應地轉爲Map。這每每是
數組轉爲JSON的逆操做。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
13.4 WeakMap
WeakMap結構與Map結構基本相似,惟一的區別是它只接受對象做爲鍵名(null除外),不接受其餘類型的值做爲鍵名,並且鍵名所指向的對象,不計
入垃圾回收機制。
var map = new WeakMap()
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
上面代碼中,若是將1和Symbol做爲WeakMap的鍵名,都會報錯。
WeakMap的設計目的在於,鍵名是對象的弱引用(垃圾回收機制不將該引用考慮在內),因此其所對應的對象可能會被自動回收。當對象被回收
後,WeakMap自動移除對應的鍵值對。典型應用是,一個對應DOM元素的WeakMap結構,當某個DOM元素被清除,其所對應的WeakMap記錄就會自動被
移除。基本上,WeakMap的專用場合就是,它的鍵所對應的對象,可能會在未來消失。WeakMap結構有助於防止內存泄漏。
下面是WeakMap結構的一個例子,能夠看到用法上與Map幾乎同樣。
var wm = new WeakMap();
var element = document.querySelector(".element");
wm.set(element, "Original");
wm.get(element) // "Original"
element.parentNode.removeChild(element);
element = null;
wm.get(element) // undefined
上面代碼中,變量wm是一個WeakMap實例,咱們將一個DOM節點element做爲鍵名,而後銷燬這個節點,element對應的鍵就自動消失了,再引用這個鍵
名就返回undefined。
WeakMap與Map在API上的區別主要是兩個,一是沒有遍歷操做(即沒有key()、values()和entries()方法),也沒有size屬性;二是沒法清空,即
不支持clear方法。這與WeakMap的鍵不被計入引用、被垃圾回收機制忽略有關。所以,WeakMap只有四個方法可
用:get()、set()、has()、delete()。
var wm = new WeakMap();
wm.size
// undefined
wm.forEach
// undefined
前文說過,WeakMap應用的典型場合就是DOM節點做爲鍵名。下面是一個例子。
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
myWeakmap.set(myElement, logoData);
}, false);
上面代碼中,myElement是一個DOM節點,每當發生click事件,就更新一下狀態。咱們將這個狀態做爲鍵值放在WeakMap裏,對應的鍵名就
是myElement。一旦這個DOM節點刪除,該狀態就會自動消失,不存在內存泄漏風險。
WeakMap的另外一個用處是部署私有屬性。
let _counter = new WeakMap();
let _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
let c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE
上面代碼中,Countdown類的兩個內部屬性_counter和_action,是實例的弱引用,因此若是刪除實例,它們也就隨之消失,不會形成內存泄漏。
14 Iterator和for...of循環
14.1 Iterator(遍歷器)的概念
JavaScript原有的表示「集合」的數據結構,主要是數組(Array)和對象(Object),ES6又添加了Map和Set。這樣就有了四種數據集合,用戶還能夠
組合使用它們,定義本身的數據結構,好比數組的成員是Map,Map的成員是對象。這樣就須要一種統一的接口機制,來處理全部不一樣的數據結構。
遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator接口,就能夠完成
遍歷操做(即依次處理該數據結構的全部成員)。
Iterator的做用有三個:一是爲各類數據結構,提供一個統一的、簡便的訪問接口;二是使得數據結構的成員可以按某種次序排列;三是ES6創造了一
種新的遍歷命令for...of循環,Iterator接口主要供for...of消費。
Iterator的遍歷過程是這樣的。
(1)建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
(2)第一次調用指針對象的next方法,能夠將指針指向數據結構的第一個成員。
(3)第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
(4)不斷調用指針對象的next方法,直到它指向數據結構的結束位置。
每一次調用next方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含value和done兩個屬性的對象。其中,value屬性是當前成
員的值,done屬性是一個布爾值,表示遍歷是否結束。
下面是一個模擬next方法返回值的例子。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
上面代碼定義了一個makeIterator函數,它是一個遍歷器生成函數,做用就是返回一個遍歷器對象。對數組['a', 'b']執行這個函數,就會返回該數
組的遍歷器對象(即指針對象)it。
指針對象的next方法,用來移動指針。開始時,指針指向數組的開始位置。而後,每次調用next方法,指針就會指向數組的下一個成員。第一次調
用,指向a;第二次調用,指向b。
next方法返回一個對象,表示當前數據成員的信息。這個對象具備value和done兩個屬性,value屬性返回當前位置的成員,done屬性是一個布爾值,
表示遍歷是否結束,便是否還有必要再一次調用next方法。
總之,調用指針對象的next方法,就能夠遍歷事先給定的數據結構。
對於遍歷器對象來講,done: false和value: undefined屬性都是能夠省略的,所以上面的makeIterator函數能夠簡寫成下面的形式。
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++]} :
{done: true};
}
};
}
因爲Iterator只是把接口規格加到數據結構之上,因此,遍歷器與它所遍歷的那個數據結構,其實是分開的,徹底能夠寫出沒有對應數據結構的遍歷
器對象,或者說用遍歷器對象模擬出數據結構。下面是一個無限運行的遍歷器對象的例子。
var it = idMaker();
it.next().value // '0'
it.next().value // '1'
it.next().value // '2'
// ...
function idMaker() {
var index = 0;
return {
next: function() {
return {value: index++, done: false};
}
};
}
上面的例子中,遍歷器生成函數idMaker,返回一個遍歷器對象(即指針對象)。可是並無對應的數據結構,或者說,遍歷器對象本身描述了一個數
據結構出來。
在ES6中,有些數據結構原生具有Iterator接口(好比數組),即不用任何處理,就能夠被for...of循環遍歷,有些就不行(好比對象)。緣由在於,
這些數據結構原生部署了Symbol.iterator屬性(詳見下文),另一些數據結構沒有。凡是部署了Symbol.iterator屬性的數據結構,就稱爲部署了
遍歷器接口。調用這個接口,就會返回一個遍歷器對象。
若是使用TypeScript的寫法,遍歷器接口(Iterable)、指針對象(Iterator)和next方法返回值的規格能夠描述以下。
interface Iterable {
[Symbol.iterator]() : Iterator,
}
interface Iterator {
next(value?: any) : IterationResult,
}
interface IterationResult {
value: any,
done: boolean,
}
14.2 數據結構的默認Iterator接口
Iterator接口的目的,就是爲全部數據結構,提供了一種統一的訪問機制,即for...of循環(詳見下文)。當使用for...of循環遍歷某種數據結構時,
該循環會自動去尋找Iterator接口。
ES6規定,默認的Iterator接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具備Symbol.iterator屬性,就能夠認爲是「可遍歷
的」(iterable)。調用Symbol.iterator方法,就會獲得當前數據結構默認的遍歷器生成函數。Symbol.iterator自己是一個表達式,返回Symbol對象
的iterator屬性,這是一個預約義好的、類型爲Symbol的特殊值,因此要放在方括號內(請參考Symbol一章)。
在ES6中,有三類數據結構原生具有Iterator接口:數組、某些相似數組的對象、Set和Map結構。
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
上面代碼中,變量arr是一個數組,原生就具備遍歷器接口,部署在arr的Symbol.iterator屬性上面。因此,調用這個屬性,就獲得遍歷器對象。
上面提到,原生就部署Iterator接口的數據結構有三類,對於這三類數據結構,不用本身寫遍歷器生成函數,for...of循環會自動遍歷它們。除此之
外,其餘數據結構(主要是對象)的Iterator接口,都須要本身在Symbol.iterator屬性上面部署,這樣纔會被for...of循環遍歷。
對象(Object)之因此沒有默認部署Iterator接口,是由於對象的哪一個屬性先遍歷,哪一個屬性後遍歷是不肯定的,須要開發者手動指定。本質上,遍歷
器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。不過,嚴格地說,對象部署遍歷器接口並非很必
要,由於這時對象實際上被看成Map結構使用,ES5沒有Map結構,而ES6原生提供了。
一個對象若是要有可被for...of循環調用的Iterator接口,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的對象具備該方法也
可)。
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value);
}
上面代碼是一個類部署Iterator接口的寫法。Symbol.iterator屬性對應一個函數,執行後返回當前對象的遍歷器對象。
下面是經過遍歷器實現指針結構的例子。
function Obj(value) {
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = {
next: next
};
var current = this;
function next() {
if (current) {
var value = current.value;
current = current.next;
return {
done: false,
value: value
};
} else {
return {
done: true
};
}
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i);
}
// 1
// 2
// 2
// 3
上面代碼首先在構造函數的原型鏈上部署Symbol.iterator方法,調用該方法會返回遍歷器對象iterator,調用該對象的next方法,在返回一個值的同
時,自動將內部指針移到下一個實例。
下面是另外一個爲對象添加Iterator接口的例子。
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
對於相似數組的對象(存在數值鍵名和length屬性),部署Iterator接口,有一個簡便方法,就是Symbol.iterator方法直接引用數組的Iterator接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 能夠執行了
下面是相似數組的對象調用數組的Symbol.iterator方法的例子。
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
注意,普通對象部署數組的Symbol.iterator方法,並沒有效果。
let iterable = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // undefined, undefined, undefined
}
若是Symbol.iterator方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function
上面代碼中,變量obj的Symbol.iterator方法對應的不是遍歷器生成函數,所以報錯。
有了遍歷器接口,數據結構就能夠用for...of循環遍歷(詳見下文),也能夠使用while循環遍歷。
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
var x = $result.value;
// ...
$result = $iterator.next();
}
上面代碼中,ITERABLE表明某種可遍歷的數據結構,$iterator是它的遍歷器對象。遍歷器對象每次移動指針(next方法),都檢查一下返回值
的done屬性,若是遍歷還沒結束,就移動遍歷器對象的指針到下一步(next方法),不斷循環。
14.3 調用Iterator接口的場合
有一些場合會默認調用Iterator接口(即Symbol.iterator方法),除了下文會介紹的for...of循環,還有幾個別的場合。
(1)解構賦值
對數組和Set結構進行解構賦值時,會默認調用Symbol.iterator方法。
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
(2)擴展運算符
擴展運算符(...)也會調用默認的iterator接口。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代碼的擴展運算符內部就調用Iterator接口。
實際上,這提供了一種簡便機制,能夠將任何部署了Iterator接口的數據結構,轉爲數組。也就是說,只要某個數據結構部署了Iterator接口,就能夠對
它使用擴展運算符,將其轉爲數組。
let arr = [...iterable];
(3)yield*
yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
(4)其餘場合
因爲數組的遍歷會調用遍歷器接口,因此任何接受數組做爲參數的場合,其實都調用了遍歷器接口。下面是一些例子。
for...of
Array.from()
Map(), Set(), WeakMap(), WeakSet()(好比new Map([['a',1],['b',2]]))
Promise.all()
Promise.race()
14.4 字符串的Iterator接口
字符串是一個相似數組的對象,也原生具備Iterator接口。
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
上面代碼中,調用Symbol.iterator方法返回一個遍歷器對象,在這個遍歷器上能夠調用next方法,實現對於字符串的遍歷。
能夠覆蓋原生的Symbol.iterator方法,達到修改遍歷器行爲的目的。
var str = new String("hi");
[...str] // ["h", "i"]
str[Symbol.iterator] = function() {
return {
next: function() {
if (this._first) {
this._first = false;
return { value: "bye", done: false };
} else {
return { done: true };
}
},
_first: true
};
};
[...str] // ["bye"]
str // "hi"
上面代碼中,字符串str的Symbol.iterator方法被修改了,因此擴展運算符(...)返回的值變成了bye,而字符串自己仍是hi。
14.5 Iterator接口與Generator函數
Symbol.iterator方法的最簡單實現,仍是使用下一章要介紹的Generator函數。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
// 或者採用下面的簡潔寫法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// hello
// world
上面代碼中,Symbol.iterator方法幾乎不用部署任何代碼,只要用yield命令給出每一步的返回值便可。
14.6 遍歷器對象的return(),throw()
遍歷器對象除了具備next方法,還能夠具備return方法和throw方法。若是你本身寫遍歷器對象生成函數,那麼next方法是必須部署的,return方法
和throw方法是否部署是可選的。
return方法的使用場合是,若是for...of循環提早退出(一般是由於出錯,或者有break語句或continue語句),就會調用return方法。若是一個對
象在完成遍歷前,須要清理或釋放資源,就能夠部署return方法。
function readLinesSync(file) {
return {
next() {
if (file.isAtEndOfFile()) {
file.close();
return { done: true };
}
},
return() {
file.close();
return { done: true };
},
};
}
上面代碼中,函數readLinesSync接受一個文件對象做爲參數,返回一個遍歷器對象,其中除了next方法,還部署了return方法。下面,咱們讓文件的
遍歷提早返回,這樣就會觸發執行return方法。
for (let line of readLinesSync(fileName)) {
console.log(x);
break;
}
注意,return方法必須返回一個對象,這是Generator規格決定的。
throw方法主要是配合Generator函數使用,通常的遍歷器對象用不到這個方法。請參閱《Generator函數》一章。
14.7 for...of循環
ES6借鑑C++、Java、C#和Python語言,引入了for...of循環,做爲遍歷全部數據結構的統一的方法。一個數據結構只要部署了Symbol.iterator屬
性,就被視爲具備iterator接口,就能夠用for...of循環遍歷它的成員。也就是說,for...of循環內部調用的是數據結構的Symbol.iterator方法。
for...of循環能夠使用的範圍包括數組、Set和Map結構、某些相似數組的對象(好比arguments對象、DOM NodeList對象)、後文的Generator對象,
以及字符串。
14.7.1 數組
數組原生具有iterator接口,for...of循環本質上就是調用這個接口產生的遍歷器,能夠用下面的代碼證實。
const arr = ['red', 'green', 'blue'];
let iterator = arr[Symbol.iterator]();
for(let v of arr) {
console.log(v); // red green blue
}
for(let v of iterator) {
console.log(v); // red green blue
}
上面代碼的for...of循環的兩種寫法是等價的。
for...of循環能夠代替數組實例的forEach方法。
const arr = ['red', 'green', 'blue'];
arr.forEach(function (element, index) {
console.log(element); // red green blue
console.log(index); // 0 1 2
});
JavaScript原有的for...in循環,只能得到對象的鍵名,不能直接獲取鍵值。ES6提供for...of循環,容許遍歷得到鍵值。
var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}
上面代碼代表,for...in循環讀取鍵名,for...of循環讀取鍵值。若是要經過for...of循環,獲取數組的索引,能夠藉助數組實例的entries方法
和keys方法,參見《數組的擴展》章節。
for...of循環調用遍歷器接口,數組的遍歷器接口只返回具備數字索引的屬性。這一點跟for...in循環也不同。
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
console.log(i); // "0", "1", "2", "foo"
}
for (let i of arr) {
console.log(i); // "3", "5", "7"
}
上面代碼中,for...of循環不會返回數組arr的foo屬性。
14.7.2 Set和Map結構
Set和Map結構也原生具備Iterator接口,能夠直接使用for...of循環。
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e);
}
// Gecko
// Trident
// Webkit
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262
上面代碼演示瞭如何遍歷Set結構和Map結構。值得注意的地方有兩個,首先,遍歷的順序是按照各個成員被添加進數據結構的順序。其次,Set結構遍
歷時,返回的是一個值,而Map結構遍歷時,返回的是一個數組,該數組的兩個成員分別爲當前Map成員的鍵名和鍵值。
let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
console.log(pair);
}
// ['a', 1]
// ['b', 2]
for (let [key, value] of map) {
console.log(key + ' : ' + value);
}
// a : 1
// b : 2
14.7.3 計算生成的數據結構
有些數據結構是在現有數據結構的基礎上,計算生成的。好比,ES6的數組、Set、Map都部署瞭如下三個方法,調用後都返回遍歷器對象。
entries() 返回一個遍歷器對象,用來遍歷[鍵名, 鍵值]組成的數組。對於數組,鍵名就是索引值;對於Set,鍵名與鍵值相同。Map結構的
iterator接口,默認就是調用entries方法。
keys() 返回一個遍歷器對象,用來遍歷全部的鍵名。
values() 返回一個遍歷器對象,用來遍歷全部的鍵值。
這三個方法調用後生成的遍歷器對象,所遍歷的都是計算生成的數據結構。
let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']
14.7.4 相似數組的對象
相似數組的對象包括好幾類。下面是for...of循環用於字符串、DOM NodeList對象、arguments對象的例子。
// 字符串
let str = "hello";
for (let s of str) {
console.log(s); // h e l l o
}
// DOM NodeList對象
let paras = document.querySelectorAll("p");
for (let p of paras) {
p.classList.add("test");
}
// arguments對象
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// 'a'
// 'b'
對於字符串來講,for...of循環還有一個特色,就是會正確識別32位UTF-16字符。
for (let x of 'a\uD83D\uDC0A') {
console.log(x);
}
// 'a'
// '\uD83D\uDC0A'
並非全部相似數組的對象都具備iterator接口,一個簡便的解決方法,就是使用Array.from方法將其轉爲數組。
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 報錯
for (let x of arrayLike) {
console.log(x);
}
// 正確
for (let x of Array.from(arrayLike)) {
console.log(x);
}
14.7.5 對象
對於普通的對象,for...of結構不能直接使用,會報錯,必須部署了iterator接口後才能使用。可是,這樣狀況下,for...in循環依然能夠用來遍歷鍵
名。
var es6 = {
edition: 6,
committee: "TC39",
standard: "ECMA-262"
};
for (e in es6) {
console.log(e);
}
// edition
// committee
// standard
for (e of es6) {
console.log(e);
}
// TypeError: es6 is not iterable
上面代碼表示,對於普通的對象,for...in循環能夠遍歷鍵名,for...of循環會報錯。
一種解決方法是,使用Object.keys方法將對象的鍵名生成一個數組,而後遍歷這個數組。
for (var key of Object.keys(someObject)) {
console.log(key + ": " + someObject[key]);
}
在對象上部署iterator接口的代碼,參見本章前面部分。一個方便的方法是將數組的Symbol.iterator屬性,直接賦值給其餘對象的Symbol.iterator屬
性。好比,想要讓for...of環遍歷jQuery對象,只要加上下面這一行就能夠了。
jQuery.prototype[Symbol.iterator] =
Array.prototype[Symbol.iterator];
另外一個方法是使用Generator函數將對象從新包裝一下。
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(obj)) {
console.log(key, "->", value);
}
// a -> 1
// b -> 2
// c -> 3
14.7.6 與其餘遍歷語法的比較
以數組爲例,JavaScript提供多種遍歷語法。最原始的寫法就是for循環。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
這種寫法比較麻煩,所以數組提供內置的forEach方法。
myArray.forEach(function (value) {
console.log(value);
});
這種寫法的問題在於,沒法中途跳出forEach循環,break命令或return命令都不能奏效。
for...in循環能夠遍歷數組的鍵名。
for (var index in myArray) {
console.log(myArray[index]);
}
for...in循環有幾個缺點。
數組的鍵名是數字,可是for...in循環是以字符串做爲鍵名「0」、「1」、「2」等等。
for...in循環不只遍歷數字鍵名,還會遍歷手動添加的其餘鍵,甚至包括原型鏈上的鍵。
某些狀況下,for...in循環會以任意順序遍歷鍵名。
總之,for...in循環主要是爲遍歷對象而設計的,不適用於遍歷數組。
for...of循環相比上面幾種作法,有一些顯著的優勢。
for (let value of myArray) {
console.log(value);
}
有着同for...in同樣的簡潔語法,可是沒有for...in那些缺點。
不一樣用於forEach方法,它能夠與break、continue和return配合使用。
提供了遍歷全部數據結構的統一操做接口。
下面是一個使用break語句,跳出for...of循環的例子。
for (var n of fibonacci) {
if (n > 1000)
break;
console.log(n);
}
上面的例子,會輸出斐波納契數列小於等於1000的項。若是當前項大於1000,就會使用break語句跳出for...of循環。
15 Generator 函數
15.1 簡介
15.1.1 基本概念
Generator函數是ES6提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。本章詳細介紹Generator函數的語法和API,它的異步編程應用請
看《異步操做》一章。
Generator函數有多種理解角度。從語法上,首先能夠把它理解成,Generator函數是一個狀態機,封裝了多個內部狀態。
執行Generator函數會返回一個遍歷器對象,也就是說,Generator函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍
歷Generator函數內部的每個狀態。
形式上,Generator函數是一個普通函數,可是有兩個特徵。一是,function關鍵字與函數名之間有一個星號;二是,函數體內部使用yield語句,定
義不一樣的內部狀態(yield語句在英語裏的意思就是「產出」)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個Generator函數helloWorldGenerator,它內部有兩個yield語句「hello」和「world」,即該函數有三個狀態:hello,world和return語
句(結束執行)。
而後,Generator函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是,調用Generator函數後,該函數並不執行,返回的
也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方
開始執行,直到遇到下一個yield語句(或return語句)爲止。換言之,Generator函數是分段執行的,yield語句是暫停執行的標記,而next方法可
以恢復執行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面代碼一共調用了四次next方法。
第一次調用,Generator函數開始執行,直到遇到第一個yield語句爲止。next方法返回一個對象,它的value屬性就是當前yield語句的值
hello,done屬性的值false,表示遍歷尚未結束。
第二次調用,Generator函數從上次yield語句停下的地方,一直執行到下一個yield語句。next方法返回的對象的value屬性就是當前yield語句的值
world,done屬性的值false,表示遍歷尚未結束。
第三次調用,Generator函數從上次yield語句停下的地方,一直執行到return語句(若是沒有return語句,就執行到函數結束)。next方法返回的對
象的value屬性,就是緊跟在return語句後面的表達式的值(若是沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷
已經結束。
第四次調用,此時Generator函數已經運行完畢,next方法返回對象的value屬性爲undefined,done屬性爲true。之後再調用next方法,返回的都是
這個值。
總結一下,調用Generator函數,返回一個遍歷器對象,表明Generator函數的內部指針。之後,每次調用遍歷器對象的next方法,就會返回一個有
着value和done兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield語句後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結
束。
ES6沒有規定,function關鍵字與函數名之間的星號,寫在哪一個位置。這致使下面的寫法都能經過。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
因爲Generator函數仍然是普通函數,因此通常的寫法是上面的第三種,即星號緊跟在function關鍵字後面。本書也採用這種寫法。
15.1.2 yield語句
因爲Generator函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield語句就是暫停
標誌。
遍歷器對象的next方法的運行邏輯以下。
(1)遇到yield語句,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。
(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield語句。
(3)若是沒有再遇到新的yield語句,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象
的value屬性值。
(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined。
須要注意的是,yield語句後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,所以等於爲JavaScript提供了手動的「惰性求
值」(Lazy Evaluation)的語法功能。
function* gen() {
yield 123 + 456;
}
上面代碼中,yield後面的表達式123 + 456,不會當即求值,只會在next方法將指針移到這一句時,纔會求值。
yield語句與return語句既有類似之處,也有區別。類似之處在於,都能返回緊跟在語句後面的那個表達式的值。區別在於每次遇到yield,函數暫停
執行,下一次再從該位置繼續向後執行,而return語句不具有位置記憶的功能。一個函數裏面,只能執行一次(或者說一個)return語句,可是能夠
執行屢次(或者說多個)yield語句。正常函數只能返回一個值,由於只能執行一次return;Generator函數能夠返回一系列的值,由於能夠有任意多
個yield。從另外一個角度看,也能夠說Generator生成了一系列的值,這也就是它的名稱的來歷(在英語中,generator這個詞是「生成器」的意思)。
Generator函數能夠不用yield語句,這時就變成了一個單純的暫緩執行函數。
function* f() {
console.log('執行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代碼中,函數f若是是普通函數,在爲變量generator賦值時就會執行。可是,函數f是一個Generator函數,就變成只有調用next方法時,函
數f纔會執行。
另外須要注意,yield語句不能用在普通函數中,不然會報錯。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
上面代碼在一個普通函數中使用yield語句,結果產生一個句法錯誤。
下面是另一個例子。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)){
console.log(f);
}
上面代碼也會產生句法錯誤,由於forEach方法的參數是一個普通函數,可是在裏面使用了yield語句(這個函數裏面還使用了yield*語句,這裏能夠
不用理會,詳細說明見後文)。一種修改方法是改用for循環。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6
另外,yield語句若是用在一個表達式之中,必須放在圓括號裏面。
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
yield語句用做函數參數或賦值表達式的右邊,能夠不加括號。
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
15.1.3 與Iterator接口的關係
上一章說過,任意一個對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。
因爲Generator函數就是遍歷器生成函數,所以能夠把Generator賦值給對象的Symbol.iterator屬性,從而使得該對象具備Iterator接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
上面代碼中,Generator函數賦值給Symbol.iterator屬性,從而使得myIterable對象具備了Iterator接口,能夠被...運算符遍歷了。
Generator函數執行後,返回一個遍歷器對象。該對象自己也具備Symbol.iterator屬性,執行後返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
上面代碼中,gen是一個Generator函數,調用它會生成一個遍歷器對象g。它的Symbol.iterator屬性,也是一個遍歷器對象生成函數,執行後返回它
本身。
15.2 next方法的參數
yield句自己沒有返回值,或者說老是返回undefined。next方法能夠帶一個參數,該參數就會被看成上一個yield語句的返回值。
function* f() {
for(var i=0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面代碼先定義了一個能夠無限運行的Generator函數f,若是next方法沒有參數,每次運行到yield語句,變量reset的值老是undefined。當next方
法帶一個參數true時,當前的變量reset就被重置爲這個參數(即true),所以i會等於-1,下一輪循環就會從-1開始遞增。
這個功能有很重要的語法意義。Generator函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。經過next方法的參數,就有辦法在
Generator函數開始運行以後,繼續向函數體內部注入值。也就是說,能夠在Generator函數運行的不一樣階段,從外部向內部注入不一樣的值,從而調整
函數行爲。
再看一個例子。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面代碼中,第二次運行next方法的時候不帶參數,致使y的值等於2 * undefined(即NaN),除以3之後仍是NaN,所以返回對象的value屬性也等
於NaN。第三次運行Next方法的時候不帶參數,因此z等於undefined,返回對象的value屬性等於5 + NaN + undefined,即NaN。
若是向next方法提供參數,返回結果就徹底不同了。上面代碼第一次調用b的next方法時,返回x+1的值6;第二次調用next方法,將上一次yield語
句的值設爲12,所以y等於24,返回y / 3的值8;第三次調用next方法,將上一次yield語句的值設爲13,所以z等於13,這時x等於5,y等於24,所
以return語句的值等於42。
注意,因爲next方法的參數表示上一個yield語句的返回值,因此第一次使用next方法時,不能帶有參數。V8引擎直接忽略第一次使用next方法時的
參數,只有從第二次使用next方法開始,參數纔是有效的。從語義上講,第一個next方法用來啓動遍歷器對象,因此不用帶有參數。
若是想要第一次調用next方法時,就可以輸入值,能夠在Generator函數外面再包一層。
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!')
// First input: hello!
上面代碼中,Generator函數若是不用wrapper先包一層,是沒法第一次調用next方法,就輸入參數的。
再看一個經過next方法的參數,向Generator函數內部輸入值的例子。
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
上面代碼是一個很直觀的例子,每次經過next方法向Generator函數輸入值,而後打印出來。
15.3 for...of循環
for...of循環能夠自動遍歷Generator函數時生成的Iterator對象,且此時再也不須要調用next方法。
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代碼使用for...of循環,依次顯示5個yield語句的值。這裏須要注意,一旦next方法的返回對象的done屬性爲true,for...of循環就會停止,且
不包含該返回對象,因此上面代碼的return語句返回的6,不包括在for...of循環之中。
下面是一個利用Generator函數和for...of循環,實現斐波那契數列的例子。
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
從上面代碼可見,使用for...of語句時不須要使用next方法。
利用for...of循環,能夠寫出遍歷任意對象(object)的方法。原生的JavaScript對象沒有遍歷接口,沒法使用for...of循環,經過Generator函數爲
它加上這個接口,就能夠用了。
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
上面代碼中,對象jane原生不具有Iterator接口,沒法用for...of遍歷。這時,咱們經過Generator函數objectEntries爲它加上遍歷器接口,就能夠
用for...of遍歷了。加上遍歷器接口的另外一種寫法是,將Generator函數加到對象的Symbol.iterator屬性上面。
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
除了for...of循環之外,擴展運算符(...)、解構賦值和Array.from方法內部調用的,都是遍歷器接口。這意味着,它們均可以將Generator函數返
回的Iterator對象,做爲參數。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 擴展運算符
[...numbers()] // [1, 2]
// Array.form 方法
Array.from(numbers()) // [1, 2]
// 解構賦值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循環
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
15.4 Generator.prototype.throw()
Generator函數返回的遍歷器對象,都有一個throw方法,能夠在函數體外拋出錯誤,而後在Generator函數體內捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
上面代碼中,遍歷器對象i連續拋出兩個錯誤。第一個錯誤被Generator函數體內的catch語句捕獲。i第二次拋出錯誤,因爲Generator函數內部
的catch語句已經執行過了,不會再捕捉到這個錯誤了,因此這個錯誤就被拋出了Generator函數體,被函數體外的catch語句捕獲。
throw方法能夠接受一個參數,該參數會被catch語句接收,建議拋出Error對象的實例。
var g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
var i = g();
i.next();
i.throw(new Error('出錯了!'));
// Error: 出錯了!(…)
注意,不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。後
者只能被函數體外的catch語句捕獲。
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('內部捕獲', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]
上面代碼之因此只捕獲了a,是由於函數體外的catch語句塊,捕獲了拋出的a錯誤之後,就不會再繼續try代碼塊裏面剩餘的語句了。
若是Generator函數內部沒有部署try...catch代碼塊,那麼throw方法拋出的錯誤,將被外部try...catch代碼塊捕獲。
var g = function* () {
while (true) {
yield;
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 a
上面代碼中,Generator函數g內部沒有部署try...catch代碼塊,因此拋出的錯誤直接被外部catch代碼塊捕獲。
若是Generator函數內部和外部,都沒有部署try...catch代碼塊,那麼程序將報錯,直接中斷執行。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
上面代碼中,g.throw拋出錯誤之後,沒有任何try...catch代碼塊能夠捕獲這個錯誤,致使程序報錯,中斷執行。
throw方法被捕獲之後,會附帶執行下一條yield語句。也就是說,會附帶執行一次next方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
上面代碼中,g.throw方法被捕獲之後,自動執行了一次next方法,因此會打印b。另外,也能夠看到,只要Generator函數內部部署了try...catch代
碼塊,那麼遍歷器的throw方法拋出的錯誤,不影響下一次遍歷。
另外,throw命令與g.throw方法是無關的,二者互不影響。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
throw new Error();
} catch (e) {
g.next();
}
// hello
// world
上面代碼中,throw命令拋出的錯誤不會影響到遍歷器的狀態,因此兩次執行next方法,都進行了正確的操做。
這種函數體內捕獲錯誤的機制,大大方便了對錯誤的處理。多個yield語句,能夠只用一個try...catch代碼塊來捕獲錯誤。若是使用回調函數的寫
法,想要捕獲多個錯誤,就不得不爲每一個函數內部寫一個錯誤處理語句,如今只在Generator函數內部寫一次catch語句就能夠了。
Generator函數體外拋出的錯誤,能夠在函數體內捕獲;反過來,Generator函數體內拋出的錯誤,也能夠被函數體外的catch捕獲。
function *foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
上面代碼中,第二個next方法向函數體內傳入一個參數42,數值是沒有toUpperCase方法的,因此會拋出一個TypeError錯誤,被函數體外的catch捕
獲。
一旦Generator執行過程當中拋出錯誤,且沒有被內部捕獲,就不會再執行下去了。若是此後還調用next方法,將返回一個value屬性等
於undefined、done屬性等於true的對象,即JavaScript引擎認爲這個Generator已經運行結束了。
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next();
console.log('第二次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next();
console.log('第三次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次運行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次運行next方法 { value: undefined, done: true }
// caller done
上面代碼一共三次運行next方法,第二次運行的時候會拋出錯誤,而後第三次運行的時候,Generator函數就已經結束了,再也不執行下去了。
15.5 Generator.prototype.return()
Generator函數返回的遍歷器對象,還有一個return方法,能夠返回給定的值,而且終結遍歷Generator函數。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
上面代碼中,遍歷器對象g調用return方法後,返回值的value屬性就是return方法的參數foo。而且,Generator函數的遍歷就終止了,返回值
的done屬性爲true,之後再調用next方法,done屬性老是返回true。
若是return方法調用時,不提供參數,則返回值的value屬性爲undefined。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
若是Generator函數內部有try...finally代碼塊,那麼return方法會推遲到finally代碼塊執行完再執行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }
上面代碼中,調用return方法後,就開始執行finally代碼塊,而後等到finally代碼塊執行完,再執行return方法。
15.6 yield*語句
若是在Generater函數內部,調用另外一個Generator函數,默認狀況下是沒有效果的。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
上面代碼中,foo和bar都是Generator函數,在bar裏面調用foo,是不會有效果的。
這個就須要用到yield*語句,用來在一個Generator函數裏面執行另外一個Generator函數。
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
再來看一個對比的例子。
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器對象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中,outer2使用了yield*,outer1沒使用。結果就是,outer1返回一個遍歷器對象,outer2返回該遍歷器對象的內部值。
從語法角度看,若是yield命令後面跟的是一個遍歷器對象,須要在yield命令後面加上星號,代表它返回的是一個遍歷器對象。這被稱爲yield*語
句。
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
上面代碼中,delegatingIterator是代理者,delegatedIterator是被代理者。因爲yield* delegatedIterator語句獲得的值,是一個遍歷器,因此要
用星號表示。運行結果就是使用一個遍歷器,遍歷了多個Generator函數,有遞歸的效果。
yield*後面的Generator函數(沒有return語句時),等同於在Generator函數內部,部署一個for...of循環。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同於
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
上面代碼說明,yield*後面的Generator函數(沒有return語句時),不過是for...of的一種簡寫形式,徹底能夠用後者替代前者。反之,則須要
用var value = yield* iterator的形式獲取return語句的值。
若是yield*後面跟着一個數組,因爲數組原生支持遍歷器,所以就會遍歷數組成員。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代碼中,yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器對象。
實際上,任何數據結構只要有Iterator接口,就能夠被yield*遍歷。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
上面代碼中,yield語句返回整個字符串,yield*語句返回單個字符。由於字符串具備Iterator接口,因此被yield*遍歷。
若是被代理的Generator函數有return語句,那麼就能夠向代理它的Generator函數返回數據。
function *foo() {
yield 2;
yield 3;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
上面代碼在第四次調用next方法的時候,屏幕上會有輸出,這是由於函數foo的return語句,向函數bar提供了返回值。
再看一個例子。
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值爲 [ 'a', 'b' ]
上面代碼中,存在兩次遍歷。第一次是擴展運算符遍歷函數logReturned返回的遍歷器對象,第二次是yield*語句遍歷函數genFuncWithReturn返回的
遍歷器對象。這兩次遍歷的效果是疊加的,最終表現爲擴展運算符遍歷函數genFuncWithReturn返回的遍歷器對象。因此,最後的數據表達式獲得的值
等於[ 'a', 'b' ]。可是,函數genFuncWithReturn的return語句的返回值The result,會返回給函數logReturned內部的result變量,所以會有終端
輸出。
yield*命令能夠很方便地取出嵌套數組的全部成員。
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
下面是一個稍微複雜的例子,使用yield*語句遍歷徹底二叉樹。
// 下面是二叉樹的構造函數,
// 三個參數分別是左樹、當前節點和右樹
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函數。
// 因爲返回的是一個遍歷器,因此要用generator函數。
// 函數體內採用遞歸算法,因此左樹和右樹要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉樹
function make(array) {
// 判斷是否爲葉節點
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
15.7 做爲對象屬性的Generator函數
若是一個對象的屬性是Generator函數,能夠簡寫成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
上面代碼中,myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個Generator函數。
它的完整形式以下,與上面的寫法是等價的。
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
15.8 Generator函數的this
Generator函數老是返回一個遍歷器,ES6規定這個遍歷器是Generator函數的實例,也繼承了Generator函數的prototype對象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代碼代表,Generator函數g返回的遍歷器obj,是g的實例,並且繼承了g.prototype。可是,若是把g看成普通的構造函數,並不會生效,因
爲g返回的老是遍歷器對象,而不是this對象。
function* g() {
this.a = 11;
}
let obj = g();
obj.a // undefined
上面代碼中,Generator函數g在this對象上面添加了一個屬性a,可是obj對象拿不到這個屬性。
Generator函數也不能跟new命令一塊兒用,會報錯。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
上面代碼中,new命令跟構造函數F一塊兒使用,結果報錯,由於F不是構造函數。
那麼,有沒有辦法讓Generator函數返回一個正常的對象實例,既能夠用next方法,又能夠得到正常的this?
下面是一個變通方法。首先,生成一個空對象,使用bind方法綁定Generator函數內部的this。這樣,構造函數調用之後,這個空對象就是Generator
函數的實例對象了。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
上面代碼中,首先是F內部的this對象綁定obj對象,而後調用它,返回一個Iterator對象。這個對象執行三次next方法(由於F內部有兩個yield語
句),完成F內部全部代碼的運行。這時,全部內部屬性都綁定在obj對象上了,所以obj對象也就成了F的實例。
上面代碼中,執行的是遍歷器對象f,可是生成的對象實例是obj,有沒有辦法將這兩個對象統一呢?
一個辦法就是將obj換成F.prototype。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
再將F改爲構造函數,就能夠對它執行new命令了。
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
15.9 含義
15.9.1 Generator與狀態機
Generator是實現狀態機的最佳結構。好比,下面的clock函數就是一個狀態機。
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
上面代碼的clock函數一共有兩種狀態(Tick和Tock),每運行一次,就改變一次狀態。這個函數若是用Generator實現,就是下面這樣。
var clock = function*() {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
上面的Generator實現與ES5實現對比,能夠看到少了用來保存狀態的外部變量ticking,這樣就更簡潔,更安全(狀態不會被非法篡改)、更符合函
數式編程的思想,在寫法上也更優雅。Generator之因此能夠不用外部變量保存狀態,是由於它自己就包含了一個狀態信息,即目前是否處於暫停態。
15.9.2 Generator與協程
協程(coroutine)是一種程序運行的方式,能夠理解成「協做的線程」或「協做的函數」。協程既能夠用單線程實現,也能夠用多線程實現。前者是一種特
殊的子例程,後者是一種特殊的線程。
(1)協程與子例程的差別
傳統的「子例程」(subroutine)採用堆棧式「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線
程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態
(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或
函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。
從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲
代價,實現多任務的並行。
(2)協程與普通線程的差別
不難看出,協程適合用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文、能夠分享全局變量。它們的不一樣之處
在於,同一時間能夠有多個線程處於運行狀態,可是運行的協程只能有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線
程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。
因爲ECMAScript是單線程語言,只能保持一個調用棧。引入協程之後,每一個任務能夠保持本身的調用棧。這樣作的最大好處,就是拋出錯誤的時候,
能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。
Generator函數是ECMAScript 6對協程的實現,但屬於不徹底實現。Generator函數被稱爲「半協程」(semi-coroutine),意思是隻有Generator函數的
調用者,才能將程序的執行權還給Generator函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。
若是將Generator函數看成協程,徹底能夠將多個須要互相協做的任務寫成Generator函數,它們之間使用yield語句交換控制權。
15.10 應用
Generator能夠暫停函數執行,返回任意表達式的值。這種特色使得Generator有多種應用場景。
(1)異步操做的同步化表達
Generator函數的暫停執行的效果,意味着能夠把異步操做寫在yield語句裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數
了,由於異步操做的後續操做能夠放在yield語句下面,反正要等到調用next方法時再執行。因此,Generator函數的一個重要實際意義就是用來處理異
步操做,改寫回調函數。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼表示,第一次調用loadUI函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading界面,而且異步
加載數據。等到數據加載完成,再一次使用next方法,則會隱藏Loading界面。能夠看到,這種寫法的好處是全部Loading界面的邏輯,都被封裝在一
個函數,循序漸進很是清晰。
Ajax是典型的異步操做,經過Generator函數部署Ajax操做,能夠用同步的方式表達。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
上面代碼的main函數,就是經過Ajax操做獲取數據。能夠看到,除了多了一個yield,它幾乎與同步操做的寫法徹底同樣。注意,makeAjaxCall函數中
的next方法,必須加上response參數,由於yield語句構成的表達式,自己是沒有值的,老是等於undefined。
下面是另外一個例子,經過Generator函數逐行讀取文本文件。
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
上面代碼打開文本文件,使用yield語句能夠手動逐行讀取文件。
(2)控制流管理
若是有一個多步操做很是耗時,採用回調函數,可能會寫成下面這樣。
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
採用Promise改寫上面的代碼。
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
上面代碼已經把回調函數,改爲了直線執行的形式,可是加入了大量Promise的語法。Generator函數能夠進一步改善代碼運行流程。
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
而後,使用一個函數,按次序自動執行全部步驟。
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 若是Generator函數未結束,就繼續調用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
注意,上面這種作法,只適合同步操做,即全部的task都必須是同步的,不能有異步操做。由於這裏的代碼一獲得返回值,就繼續往下執行,沒有判
斷異步操做什麼時候完成。若是要控制異步的操做流程,詳見後面的《異步操做》一章。
下面,利用for...of循環會自動依次執行yield命令的特性,提供一種更通常的控制流管理的方法。
let steps = [step1Func, step2Func, step3Func];
function *iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
上面代碼中,數組steps封裝了一個任務的多個步驟,Generator函數iterateSteps則是依次爲這些步驟加上yield命令。
將任務分解成步驟以後,還能夠將項目分解成多個依次執行的任務。
let jobs = [job1, job2, job3];
function *iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){
var job = jobs[i];
yield *iterateSteps(job.steps);
}
}
上面代碼中,數組jobs封裝了一個項目的多個任務,Generator函數iterateJobs則是依次爲這些任務加上yield *命令。
最後,就能夠用for...of循環一次性依次執行全部任務的全部步驟。
for (var step of iterateJobs(jobs)){
console.log(step.id);
}
再次提醒,上面的作法只能用於全部步驟都是同步操做的狀況,不能有異步操做的步驟。若是想要依次執行異步的步驟,必須使用後面的《異步操
做》一章介紹的方法。
for...of的本質是一個while循環,因此上面的代碼實質上執行的是下面的邏輯。
var it = iterateJobs(jobs);
var res = it.next();
while (!res.done){
var result = res.value;
// ...
res = it.next();
}
(3)部署Iterator接口
利用Generator函數,能夠在任意對象上部署Iterator接口。
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
上述代碼中,myObj是一個普通對象,經過iterEntries函數,就有了Iterator接口。也就是說,能夠在任意對象上部署next方法。
下面是一個對數組部署Iterator接口的例子,儘管數組原生具備這個接口。
function* makeSimpleGenerator(array){
var nextIndex = 0;
while(nextIndex < array.length){
yield array[nextIndex++];
}
}
var gen = makeSimpleGenerator(['yo', 'ya']);
gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done // true
(4)做爲數據結構
Generator能夠看做是數據結構,更確切地說,能夠看做是一個數組結構,由於Generator函數能夠返回一系列的值,這意味着它能夠對任意表達式,
提供相似數組的接口。
function *doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
上面代碼就是依次返回三個函數,可是因爲使用了Generator函數,致使能夠像處理數組那樣,處理這三個返回的函數。
for (task of doStuff()) {
// task是一個函數,能夠像回調函數那樣使用它
}
實際上,若是用ES5表達,徹底能夠用數組模擬Generator的這種用法。
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
上面的函數,能夠用如出一轍的for...of循環處理!兩相一比較,就不難看出Generator使得數據或者操做,具有了相似數組的接口。
16 Promise對象
16.1 Promise的含義
Promise是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6將其寫進了語言標
準,統一了用法,原生提供了Promise對象。
所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。從語法上說,Promise是一個對象,從它
能夠獲取異步操做的消息。Promise提供統一的API,各類異步操做均可以用一樣的方法進行處理。
Promise對象有如下兩個特色。
(1)對象的狀態不受外界影響。Promise對象表明一個異步操做,有三種狀態:Pending(進行中)、Resolved(已完成,又稱Fulfilled)
和Rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它
的英語意思就是「承諾」,表示其餘手段沒法改變。
(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變
爲Rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對Promise對象添加回調函數,也
會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。
有了Promise對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步
操做更加容易。
Promise也有一些缺點。首先,沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。其次,若是不設置回調函數,Promise內部拋出的錯誤,
不會反應到外部。第三,當處於Pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。
若是某些事件不斷地反覆發生,通常來講,使用stream模式是比部署Promise更好的選擇。
16.2 基本用法
ES6規定,Promise對象是一個構造函數,用來生成Promise實例。
下面代碼創造了一個Promise實例。
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操做成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由JavaScript引擎提供,不用本身部署。
resolve函數的做用是,將Promise對象的狀態從「未完成」變爲「成功」(即從Pending變爲Resolved),在異步操做成功時調用,並將異步操做的結果,
做爲參數傳遞出去;reject函數的做用是,將Promise對象的狀態從「未完成」變爲「失敗」(即從Pending變爲Rejected),在異步操做失敗時調用,並將
異步操做報出的錯誤,做爲參數傳遞出去。
Promise實例生成之後,能夠用then方法分別指定Resolved狀態和Reject狀態的回調函數。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then方法能夠接受兩個回調函數做爲參數。第一個回調函數是Promise對象的狀態變爲Resolved時調用,第二個回調函數是Promise對象的狀態變爲
Reject時調用。其中,第二個函數是可選的,不必定要提供。這兩個函數都接受Promise對象傳出的值做爲參數。
下面是一個Promise對象的簡單例子。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
上面代碼中,timeout方法返回一個Promise實例,表示一段時間之後纔會發生的結果。過了指定的時間(ms參數)之後,Promise實例的狀態變爲
Resolved,就會觸發then方法綁定的回調函數。
Promise新建後就會當即執行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('Resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// Resolved
上面代碼中,Promise新建後當即執行,因此首先輸出的是「Promise」。而後,then方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執
行,因此「Resolved」最後輸出。
下面是異步加載圖片的例子。
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
var image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
上面代碼中,使用Promise包裝了一個圖片加載的異步操做。若是加載成功,就調用resolve方法,不然就調用reject方法。
下面是一個用Promise對象實現的Ajax操做的例子。
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出錯了', error);
});
上面代碼中,getJSON是對XMLHttpRequest對象的封裝,用於發出一個針對JSON數據的HTTP請求,而且返回一個Promise對象。須要注意的是,
在getJSON內部,resolve函數和reject函數調用時,都帶有參數。
若是調用resolve函數和reject函數時帶有參數,那麼它們的參數會被傳遞給回調函數。reject函數的參數一般是Error對象的實例,表示拋出的錯
誤;resolve函數的參數除了正常的值之外,還多是另外一個Promise實例,表示異步操做的結果有多是一個值,也有多是另外一個異步操做,好比
像下面這樣。
var p1 = new Promise(function (resolve, reject) {
// ...
});
var p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
上面代碼中,p1和p2都是Promise的實例,可是p2的resolve方法將p1做爲參數,即一個異步操做的結果是返回另外一個異步操做。
注意,這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態。若是p1的狀態是Pending,那麼p2的回調函數就會等待p1的狀態改變;如
果p1的狀態已是Resolved或者Rejected,那麼p2的回調函數將會馬上執行。
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
上面代碼中,p1是一個Promise,3秒以後變爲rejected。p2的狀態在1秒以後改變,resolve方法返回的是p1。此時,因爲p2返回的是另外一個
Promise,因此後面的then語句都變成針對後者(p1)。又過了2秒,p1變爲rejected,致使觸發catch方法指定的回調函數。
16.3 Promise.prototype.then()
Promise實例具備then方法,也就是說,then方法是定義在原型對象Promise.prototype上的。它的做用是爲Promise實例添加狀態改變時的回調函數。
前面說過,then方法的第一個參數是Resolved狀態的回調函數,第二個參數(可選)是Rejected狀態的回調函數。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數。
採用鏈式的then,能夠指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的仍是一個Promise對象(即有異步操做),這時後一
個回調函數,就會等待該Promise對象的狀態發生變化,纔會被調用。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function funcA(comments) {
console.log("Resolved: ", comments);
}, function funcB(err){
console.log("Rejected: ", err);
});
上面代碼中,第一個then方法指定的回調函數,返回的是另外一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對
象狀態發生變化。若是變爲Resolved,就調用funcA,若是狀態變爲Rejected,就調用funcB。
若是採用箭頭函數,上面的代碼能夠寫得更簡潔。
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("Resolved: ", comments),
err => console.log("Rejected: ", err)
);
16.4 Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。
getJSON("/posts.json").then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回調函數運行時發生的錯誤
console.log('發生錯誤!', error);
});
上面代碼中,getJSON方法返回一個Promise對象,若是該對象狀態變爲Resolved,則會調用then方法指定的回調函數;若是異步操做拋出錯誤,狀態
就會變爲Rejected,就會調用catch方法指定的回調函數,處理這個錯誤。另外,then方法指定的回調函數,若是運行中拋出錯誤,也會被catch方法
捕獲。
p.then((val) => console.log("fulfilled:", val))
.catch((err) => console.log("rejected:", err));
// 等同於
p.then((val) => console.log("fulfilled:", val))
.then(null, (err) => console.log("rejected:", err));
下面是一個例子。
var promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
上面代碼中,promise拋出一個錯誤,就被catch方法指定的回調函數捕獲。注意,上面的寫法與下面兩種寫法是等價的。
// 寫法一
var promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 寫法二
var promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
比較上面兩種寫法,能夠發現reject方法的做用,等同於拋出錯誤。
若是Promise狀態已經變成Resolved,再拋出錯誤是無效的。
var promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代碼中,Promise在resolve語句後面,再拋出錯誤,不會被捕獲,等於沒有拋出。
Promise對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 處理前面三個Promise產生的錯誤
});
上面代碼中,一共有三個Promise對象:一個由getJSON產生,兩個由then產生。它們之中任何一個拋出的錯誤,都會被最後一個catch捕獲。
通常來講,不要在then方法裏面定義Reject狀態的回調函數(即then的第二個參數),老是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
上面代碼中,第二種寫法要好於第一種寫法,理由是第二種寫法能夠捕獲前面then方法執行中的錯誤,也更接近同步的寫法(try/catch)。所以,建
議老是使用catch方法,而不使用then方法的第二個參數。
跟傳統的try/catch代碼塊不一樣的是,若是沒有使用catch方法指定錯誤處理的回調函數,Promise對象拋出的錯誤不會傳遞到外層代碼,即不會有任何
反應。
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,由於x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
上面代碼中,someAsyncThing函數產生的Promise對象會報錯,可是因爲沒有指定catch方法,這個錯誤不會被捕獲,也不會傳遞到外層代碼,致使運
行後沒有任何輸出。注意,Chrome瀏覽器不遵照這條規定,它會拋出錯誤「ReferenceError: x is not defined」。
var promise = new Promise(function(resolve, reject) {
resolve("ok");
setTimeout(function() { throw new Error('test') }, 0)
});
promise.then(function(value) { console.log(value) });
// ok
// Uncaught Error: test
上面代碼中,Promise指定在下一輪「事件循環」再拋出錯誤,結果因爲沒有指定使用try...catch語句,就冒泡到最外層,成了未捕獲的錯誤。由於此
時,Promise的函數體已經運行結束了,因此這個錯誤是在Promise函數體外拋出的。
Node.js有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤。
process.on('unhandledRejection', function (err, p) {
console.error(err.stack)
});
上面代碼中,unhandledRejection事件的監聽函數有兩個參數,第一個是錯誤對象,第二個是報錯的Promise實例,它能夠用來了解發生錯誤的環境信
息。。
須要注意的是,catch方法返回的仍是一個Promise對象,所以後面還能夠接着調用then方法。
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,由於x沒有聲明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
上面代碼運行完catch方法指定的回調函數,會接着運行後面那個then方法指定的回調函數。若是沒有報錯,則會跳過catch方法。
Promise.resolve()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// carry on
上面的代碼由於沒有報錯,跳過了catch方法,直接執行後面的then方法。此時,要是then方法裏面報錯,就與前面的catch無關了。
catch方法之中,還能再拋出錯誤。
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,由於x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行會報錯,由於y沒有聲明
y + 2;
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
上面代碼中,catch方法拋出一個錯誤,由於後面沒有別的catch方法了,致使這個錯誤不會被捕獲,也不會傳遞到外層。若是改寫一下,結果就不一
樣了。
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行會報錯,由於y沒有聲明
y + 2;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
上面代碼中,第二個catch方法用來捕獲,前一個catch方法拋出的錯誤。
16.5 Promise.all()
Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.all方法接受一個數組做爲參數,p一、p二、p3都是Promise對象的實例,若是不是,就會先調用下面講到的Promise.resolve方
法,將參數轉爲Promise實例,再進一步處理。(Promise.all方法的參數能夠不是數組,但必須具備Iterator接口,且返回的每一個成員都是Promise實
例。)
p的狀態由p一、p二、p3決定,分紅兩種狀況。
(1)只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
下面是一個具體的例子。
// 生成一個Promise對象的數組
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON("/post/" + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
上面代碼中,promises是包含6個Promise實例的數組,只有這6個實例的狀態都變成fulfilled,或者其中有一個變爲rejected,纔會調
用Promise.all方法後面的回調函數。
下面是另外一個例子。
const databasePromise = connectDatabase();
const booksPromise = databaseProimse
.then(findAllBooks);
const userPromise = databasePromise
.then(getCurrentUser);
Promise.all([
booksPromise,
userPromise
])
.then(([books, user]) => pickTopRecommentations(books, user));
上面代碼中,booksPromise和userPromise是兩個異步操做,只有等到它們的結果都返回了,纔會觸發pickTopRecommentations這個回調函數。
16.6 Promise.race()
Promise.race方法一樣是將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.race([p1,p2,p3]);
上面代碼中,只要p一、p二、p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的Promise實例的返回值,就傳遞給p的回調函數。
Promise.race方法的參數與Promise.all方法同樣,若是不是Promise實例,就會先調用下面講到的Promise.resolve方法,將參數轉爲Promise實例,
再進一步處理。
下面是一個例子,若是指定時間內沒有得到結果,就將Promise的狀態變爲reject,不然變爲resolve。
var p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(response => console.log(response))
p.catch(error => console.log(error))
上面代碼中,若是5秒以內fetch方法沒法返回結果,變量p的狀態就會變爲rejected,從而觸發catch方法指定的回調函數。
16.7 Promise.resolve()
有時須要將現有對象轉爲Promise對象,Promise.resolve方法就起到這個做用。
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代碼將jQuery生成的deferred對象,轉爲一個新的Promise對象。
Promise.resolve等價於下面的寫法。
Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
Promise.resolve方法的參數分紅四種狀況。
(1)參數是一個Promise實例
若是參數是Promise實例,那麼Promise.resolve將不作任何修改、原封不動地返回這個實例。
(2)參數是一個 thenable對象
thenable對象指的是具備then方法的對象,好比下面這個對象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve方法會將這個對象轉爲Promise對象,而後就當即執行thenable對象的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
上面代碼中,thenable對象的then方法執行後,對象p1的狀態就變爲resolved,從而當即執行最後那個then方法指定的回調函數,輸出42。
(3)參數不是具備 then方法的對象,或根本就不是對象
若是參數是一個原始值,或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態爲Resolved。
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代碼生成一個新的Promise對象的實例p。因爲字符串Hello不屬於異步操做(判斷方法是它不是具備then方法的對象),返回Promise實例的狀態
從一輩子成就是Resolved,因此回調函數會當即執行。Promise.resolve方法的參數,會同時傳給回調函數。
(4)不帶有任何參數
Promise.resolve方法容許調用時不帶參數,直接返回一個Resolved狀態的Promise對象。
因此,若是但願獲得一個Promise對象,比較方便的方法就是直接調用Promise.resolve方法。
var p = Promise.resolve();
p.then(function () {
// ...
});
上面代碼的變量p就是一個Promise對象。
須要注意的是,當即resolve的Promise對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
上面代碼中,setTimeout(fn, 0)在下一輪「事件循環」開始時執行,Promise.resolve()在本輪「事件循環」結束時執行,console.log(’one‘)則是當即執
行,所以最早輸出。
16.8 Promise.reject()
Promise.reject(reason)方法也會返回一個新的Promise實例,該實例的狀態爲rejected。它的參數用法與Promise.resolve方法徹底一致。
var p = Promise.reject('出錯了');
// 等同於
var p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s){
console.log(s)
});
// 出錯了
上面代碼生成一個Promise對象的實例p,狀態爲rejected,回調函數會當即執行。
16.9 兩個有用的附加方法
ES6的Promise API提供的方法不是不少,有些有用的方法能夠本身部署。下面介紹如何部署兩個不在ES6之中、但頗有用的方法。
16.9.1 done()
Promise對象的回調鏈,無論以then方法或catch方法結尾,要是最後一個方法拋出錯誤,都有可能沒法捕捉到(由於Promise內部的錯誤不會冒泡到
全局)。所以,咱們能夠提供一個done方法,老是處於回調鏈的尾端,保證拋出任何可能出現的錯誤。
asyncFunc()
.then(f1)
.catch(r1)
.then(f2)
.done();
它的實現代碼至關簡單。
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 拋出一個全局錯誤
setTimeout(() => { throw reason }, 0);
});
};
從上面代碼可見,done方法的使用,能夠像then方法那樣用,提供Fulfilled和Rejected狀態的回調函數,也能夠不提供任何參數。但無論怎
樣,done都會捕捉到任何可能出現的錯誤,並向全局拋出。
16.9.2 finally()
finally方法用於指定無論Promise對象最後狀態如何,都會執行的操做。它與done方法的最大區別,它接受一個普通的回調函數做爲參數,該函數不
管怎樣都必須執行。
下面是一個例子,服務器使用Promise處理請求,而後使用finally方法關掉服務器。
server.listen(0)
.then(function () {
// run test
})
.finally(server.stop);
它的實現也很簡單。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
上面代碼中,無論前面的Promise是fulfilled仍是rejected,都會執行回調函數callback。
16.10 應用
16.10.1 加載圖片
咱們能夠將圖片的加載寫成一個Promise,一旦加載完成,Promise的狀態就發生變化。
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
16.10.2 Generator函數與Promise的結合
使用Generator函數管理流程,遇到異步操做的時候,一般返回一個Promise對象。
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
var g = function* () {
try {
var foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
var it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
上面代碼的Generator函數g之中,有一個異步操做getFoo,它返回的就是一個Promise對象。函數run用來處理這個Promise對象,並調用下一
個next方法。
17 異步操做和Async函數
異步編程對JavaScript語言過重要。Javascript語言的執行環境是「單線程」的,若是沒有異步編程,根本無法用,非卡死不可。
ES6誕生之前,異步編程的方法,大概有下面四種。
回調函數
事件監聽
發佈/訂閱
Promise 對象
ES6將JavaScript異步編程帶入了一個全新的階段,ES7的Async函數更是提出了異步編程的終極解決方案。
17.1 基本概念
17.1.1 異步
所謂"異步",簡單說就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。
好比,有一個任務是讀取文件進行處理,任務的第一段是向操做系統發出請求,要求讀取文件。而後,程序執行其餘任務,等到操做系統返回文件,
再接着執行任務的第二段(處理文件)。這種不連續的執行,就叫作異步。
相應地,連續的執行就叫作同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着。
17.1.2 回調函數
JavaScript語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏面,等到從新執行這個任務的時候,就直
接調用這個函數。它的英語名字callback,直譯過來就是"從新調用"。
讀取文件進行處理,是這樣寫的。
fs.readFile('/etc/passwd', function (err, data) {
if (err) throw err;
console.log(data);
});
上面代碼中,readFile函數的第二個參數,就是回調函數,也就是任務的第二段。等到操做系統返回了/etc/passwd這個文件之後,回調函數纔會執
行。
一個有趣的問題是,爲何Node.js約定,回調函數的第一個參數,必須是錯誤對象err(若是沒有錯誤,該參數就是null)?緣由是執行分紅兩段,在
這兩段之間拋出的錯誤,程序沒法捕捉,只能看成參數,傳入第二段。
17.1.3 Promise
回調函數自己並無問題,它的問題出如今多個回調函數嵌套。假定讀取A文件以後,再讀取B文件,代碼以下。
fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) {
// ...
});
});
不難想象,若是依次讀取多個文件,就會出現多重嵌套。代碼不是縱向發展,而是橫向發展,很快就會亂成一團,沒法管理。這種狀況就稱爲"回調函
數噩夢"(callback hell)。
Promise就是爲了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,容許將回調函數的嵌套,改爲鏈式調用。採用Promise,連續讀
取多個文件,寫法以下。
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function(data){
console.log(data.toString());
})
.then(function(){
return readFile(fileB);
})
.then(function(data){
console.log(data.toString());
})
.catch(function(err) {
console.log(err);
});
上面代碼中,我使用了fs-readfile-promise模塊,它的做用就是返回一個Promise版本的readFile函數。Promise提供then方法加載回調函數,catch方法
捕捉執行過程當中拋出的錯誤。
能夠看到,Promise 的寫法只是回調函數的改進,使用then方法之後,異步任務的兩段執行看得更清楚了,除此之外,並沒有新意。
Promise 的最大問題是代碼冗餘,原來的任務被Promise 包裝了一下,無論什麼操做,一眼看去都是一堆 then,原來的語義變得很不清楚。
那麼,有沒有更好的寫法呢?
17.2 Generator函數
17.2.1 協程
傳統的編程語言,早有異步編程的解決方案(實際上是多任務的解決方案)。其中有一種叫作"協程"(coroutine),意思是多個線程互相協做,完成異步
任務。
協程有點像函數,又有點像線程。它的運行流程大體以下。
第一步,協程A開始執行。
第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
第三步,(一段時間後)協程B交還執行權。
第四步,協程A恢復執行。
上面流程的協程A,就是異步任務,由於它分紅兩段(或多段)執行。
舉例來講,讀取文件的協程寫法以下。
function *asyncJob() {
// ...其餘代碼
var f = yield readFile(fileA);
// ...其餘代碼
}
上面代碼的函數asyncJob是一個協程,它的奧妙就在其中的yield命令。它表示執行到此處,執行權將交給其餘協程。也就是說,yield命令是異步兩
個階段的分界線。
協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續日後執行。它的最大優勢,就是代碼的寫法很是像同步操做,若是去除yield命令,
簡直如出一轍。
17.2.3 Generator函數的概念
Generator函數是協程在ES6的實現,最大特色就是能夠交出函數的執行權(即暫停執行)。
整個Generator函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操做須要暫停的地方,都用yield語句註明。Generator函數的執行方法
以下。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面代碼中,調用Generator函數,會返回一個內部指針(即遍歷器)g 。這是Generator函數不一樣於普通函數的另外一個地方,即執行它不會返回結
果,返回的是指針對象。調用指針g的next方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的yield語句,上例是執行到x + 2爲
止。
換言之,next方法的做用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value
屬性是yield語句後面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,便是否還有下一個階段。
17.2.4 Generator函數的數據交換和錯誤處理
Generator函數能夠暫停執行和恢復執行,這是它能封裝異步任務的根本緣由。除此以外,它還有兩個特性,使它能夠做爲異步編程的完整解決方案:
函數體內外的數據交換和錯誤處理機制。
next方法返回值的value屬性,是Generator函數向外輸出數據;next方法還能夠接受參數,這是向Generator函數體內輸入數據。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
上面代碼中,第一個next方法的value屬性,返回表達式x + 2的值(3)。第二個next方法帶有參數2,這個參數能夠傳入 Generator 函數,做爲上個
階段異步任務的返回結果,被函數體內的變量y接收。所以,這一步的 value 屬性,返回的就是2(變量y的值)。
Generator 函數內部還能夠部署錯誤處理代碼,捕獲函數體外拋出的錯誤。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出錯了');
// 出錯了
上面代碼的最後一行,Generator函數體外,使用指針對象的throw方法拋出的錯誤,能夠被函數體內的try ...catch代碼塊捕獲。這意味着,出錯的代碼
與處理錯誤的代碼,實現了時間和空間上的分離,這對於異步編程無疑是很重要的。
17.2.5異步任務的封裝
下面看看如何使用 Generator 函數,執行一個真實的異步任務。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
上面代碼中,Generator函數封裝了一個異步操做,該操做先讀取一個遠程接口,而後從JSON格式的數據解析信息。就像前面說過的,這段代碼很是
像同步操做,除了加上了yield命令。
執行這段代碼的方法以下。
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面代碼中,首先執行Generator函數,獲取遍歷器對象,而後使用next 方法(第二行),執行異步任務的第一階段。因爲Fetch模塊返回的是一個
Promise對象,所以要用then方法調用下一個next 方法。
能夠看到,雖然 Generator 函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。
17.3 Thunk函數
17.3.1 參數的求值策略
Thunk函數早在上個世紀60年代就誕生了。
那時,編程語言剛剛起步,計算機學家還在研究,編譯器怎麼寫比較好。一個爭論的焦點是"求值策略",即函數的參數到底應該什麼時候求值。
var x = 1;
function f(m){
return m * 2;
}
f(x + 5)
上面代碼先定義函數f,而後向它傳入表達式x + 5。請問,這個表達式應該什麼時候求值?
一種意見是"傳值調用"(call by value),即在進入函數體以前,就計算x + 5的值(等於6),再將這個值傳入函數f 。C語言就採用這種策略。
f(x + 5)
// 傳值調用時,等同於
f(6)
另外一種意見是"傳名調用"(call by name),即直接將表達式x + 5傳入函數體,只在用到它的時候求值。Haskell語言採用這種策略。
f(x + 5)
// 傳名調用時,等同於
(x + 5) * 2
傳值調用和傳名調用,哪種比較好?回答是各有利弊。傳值調用比較簡單,可是對參數求值的時候,實際上還沒用到這個參數,有可能形成性能損
失。
function f(a, b){
return b;
}
f(3 * x * x - 2 * x - 1, x);
上面代碼中,函數f的第一個參數是一個複雜的表達式,可是函數體內根本沒用到。對這個參數求值,其實是沒必要要的。所以,有一些計算機學家傾
向於"傳名調用",即只在執行時求值。
17.3.2 Thunk函數的含義
編譯器的"傳名調用"實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作Thunk函數。
function f(m){
return m * 2;
}
f(x + 5);
// 等同於
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
上面代碼中,函數f的參數x + 5被一個函數替換了。凡是用到原參數的地方,對Thunk函數求值便可。
這就是Thunk函數的定義,它是"傳名調用"的一種實現策略,用來替換某個表達式。
17.3.3 JavaScript語言的Thunk函數
JavaScript語言是傳值調用,它的Thunk函數含義有所不一樣。在JavaScript語言中,Thunk函數替換的不是表達式,而是多參數函數,將其替換成單參
數的版本,且只接受回調函數做爲參數。
// 正常版本的readFile(多參數版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(單參數版本)
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
var Thunk = function (fileName){
return function (callback){
return fs.readFile(fileName, callback);
};
};
上面代碼中,fs模塊的readFile方法是一個多參數函數,兩個參數分別爲文件名和回調函數。通過轉換器處理,它變成了一個單參數函數,只接受回調
函數做爲參數。這個單參數版本,就叫作Thunk函數。
任何函數,只要參數有回調函數,就能寫成Thunk函數的形式。下面是一個簡單的Thunk函數轉換器。
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
var Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
使用上面的轉換器,生成fs.readFile的Thunk函數。
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
下面是另外一個完整的例子。
function f(a, cb) {
cb(a);
}
let ft = Thunk(f);
let log = console.log.bind(console);
ft(1)(log) // 1
17.3.4 Thunkify模塊
生產環境的轉換器,建議使用Thunkify模塊。
首先是安裝。
$ npm install thunkify
使用方式以下。
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
Thunkify的源碼與上一節那個簡單的轉換器很是像。
function thunkify(fn){
return function(){
var args = new Array(arguments.length);
var ctx = this;
for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function(done){
var called;
args.push(function(){
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
它的源碼主要多了一個檢查機制,變量called確保回調函數只運行一次。這樣的設計與下文的Generator函數相關。請看下面的例子。
function f(a, b, callback){
var sum = a + b;
callback(sum);
callback(sum);
}
var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3
上面代碼中,因爲thunkify只容許回調函數執行一次,因此只輸出一行結果。
17.3.5 Generator 函數的流程管理
你可能會問, Thunk函數有什麼用?回答是之前確實沒什麼用,可是ES6有了Generator函數,Thunk函數如今能夠用於Generator函數的自動流程管
理。
Generator函數能夠自動執行。
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
上面代碼中,Generator函數gen會自動執行完全部步驟。
可是,這不適合異步操做。若是必須保證前一步執行完,才能執行後一步,上面的自動執行就不可行。這時,Thunk函數就能派上用處。以讀取文件爲
例。下面的Generator函數封裝了兩個異步操做。
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFile('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFile('/etc/shells');
console.log(r2.toString());
};
上面代碼中,yield命令用於將程序的執行權移出Generator函數,那麼就須要一種方法,將執行權再交還給Generator函數。
這種方法就是Thunk函數,由於它能夠在回調函數裏,將執行權交還給Generator函數。爲了便於理解,咱們先看如何手動執行上面這個Generator函
數。
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
if (err) throw err;
g.next(data);
});
});
上面代碼中,變量g是Generator函數的內部指針,表示目前執行到哪一步。next方法負責將指針移動到下一步,並返回該步的信息(value屬性和done
屬性)。
仔細查看上面的代碼,能夠發現Generator函數的執行過程,實際上是將同一個回調函數,反覆傳入next方法的value屬性。這使得咱們能夠用遞歸來自動
完成這個過程。
17.3.6 Thunk函數的自動流程管理
Thunk函數真正的威力,在於能夠自動執行Generator函數。下面就是一個基於Thunk函數的Generator執行器。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
上面代碼的run函數,就是一個Generator函數的自動執行器。內部的next函數就是Thunk的回調函數。next函數先將指針移到Generator函數的下一
步(gen.next方法),而後判斷Generator函數是否結束(result.done屬性),若是沒結束,就將next函數再傳入Thunk函數(result.value屬
性),不然就直接退出。
有了這個執行器,執行Generator函數方便多了。無論內部有多少個異步操做,直接把Generator函數傳入run函數便可。固然,前提是每個異步操
做,都要是Thunk函數,也就是說,跟在yield命令後面的必須是Thunk函數。
var g = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};
run(g);
上面代碼中,函數g封裝了n個異步的讀取文件操做,只要執行run函數,這些操做就會自動完成。這樣一來,異步操做不只能夠寫得像同步操做,並且
一行代碼就能夠執行。
Thunk函數並非Generator函數自動執行的惟一方案。由於自動執行的關鍵是,必須有一種機制,自動控制Generator函數的流程,接收和交還程序
的執行權。回調函數能夠作到這一點,Promise 對象也能夠作到這一點。
17.4 co模塊
17.4.1 基本用法
co模塊是著名程序員TJ Holowaychuk於2013年6月發佈的一個小工具,用於Generator函數的自動執行。
好比,有一個Generator函數,用於依次讀取兩個文件。
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co模塊可讓你不用編寫Generator函數的執行器。
var co = require('co');
co(gen);
上面代碼中,Generator函數只要傳入co函數,就會自動執行。
co函數返回一個Promise對象,所以能夠用then方法添加回調函數。
co(gen).then(function (){
console.log('Generator 函數執行完成');
});
上面代碼中,等到Generator函數執行結束,就會輸出一行提示。
17.4.2 co模塊的原理
爲何co能夠自動執行Generator函數?
前面說過,Generator就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。
兩種方法能夠作到這一點。
(1)回調函數。將異步操做包裝成Thunk函數,在回調函數裏面交回執行權。
(2)Promise 對象。將異步操做包裝成Promise對象,用then方法交回執行權。
co模塊其實就是將兩種自動執行器(Thunk函數和Promise對象),包裝成一個模塊。使用co的前提條件是,Generator函數的yield命令後面,只能是
Thunk函數或Promise對象。
上一節已經介紹了基於Thunk函數的自動執行器。下面來看,基於Promise對象的自動執行器。這是理解co模塊必須的。
17.4.3 基於Promise對象的自動執行
仍是沿用上面的例子。首先,把fs模塊的readFile方法包裝成一個Promise對象。
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
而後,手動執行上面的Generator函數。
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
手動執行其實就是用then方法,層層添加回調函數。理解了這一點,就能夠寫出一個自動執行器。
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
上面代碼中,只要Generator函數還沒執行到最後一步,next函數就調用自身,以此實現自動執行。
17.4.4 co模塊的源碼
co就是上面那個自動執行器的擴展,它的源碼只有幾十行,很是簡單。
首先,co函數接受Generator函數做爲參數,返回一個 Promise 對象。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
在返回的Promise對象裏面,co先檢查參數gen是否爲Generator函數。若是是,就執行該函數,獲得一個內部指針對象;若是不是就返回,並將
Promise對象的狀態改成resolved。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
接着,co將Generator函數的內部指針對象的next方法,包裝成onFulfilled函數。這主要是爲了可以捕捉拋出的錯誤。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
最後,就是關鍵的next函數,它會反覆調用自身。
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
上面代碼中,next 函數的內部代碼,一共只有四行命令。
第一行,檢查當前是否爲 Generator 函數的最後一步,若是是就返回。
第二行,確保每一步的返回值,是 Promise 對象。
第三行,使用 then 方法,爲返回值加上回調函數,而後經過 onFulfilled 函數再次調用 next 函數。
第四行,在參數不符合要求的狀況下(參數非 Thunk 函數和 Promise 對象),將 Promise 對象的狀態改成 rejected,從而終止執行。
17.4.5 處理併發的異步操做
co支持併發的異步操做,即容許某些操做同時進行,等到它們所有完成,才進行下一步。
這時,要把併發的操做都放在數組或對象裏面,跟在yield語句後面。
// 數組的寫法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
// 對象的寫法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
下面是另外一個例子。
co(function* () {
var values = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y
}
上面的代碼容許併發三個somethingAsync異步操做,等到它們所有完成,纔會進行下一步。
17.5 async函數
17.5.1 含義
ES7提供了async函數,使得異步操做變得更加方便。async函數是什麼?一句話,async函數就是Generator函數的語法糖。
前文有一個Generator函數,依次讀取兩個文件。
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
寫成async函數,就是下面這樣。
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比較就會發現,async函數就是將Generator函數的星號(*)替換成async,將yield替換成await,僅此而已。
async函數對 Generator 函數的改進,體如今如下四點。
(1)內置執行器。Generator函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說,async函數的執行,與普通函數一
模同樣,只要一行。
var result = asyncReadFile();
上面的代碼調用了asyncReadFile函數,而後它就會自動執行,輸出最後結果。這徹底不像Generator函數,須要調用next方法,或者用co模塊,才能
獲得真正執行,獲得最後結果。
(2)更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
(3)更廣的適用性。 co模塊約定,yield命令後面只能是Thunk函數或Promise對象,而async函數的await命令後面,能夠是Promise對象和原始類
型的值(數值、字符串和布爾值,但這時等同於同步操做)。
(4)返回值是Promise。async函數的返回值是Promise對象,這比Generator函數的返回值是Iterator對象方便多了。你能夠用then方法指定下一步的
操做。
進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個Promise對象,而await命令就是內部then命令的語法糖。
17.5.2 語法
async函數的語法規則整體上比較簡單,難點是錯誤處理機制。
(1)async函數返回一個Promise對象。
async函數內部return語句返回的值,會成爲then方法回調函數的參數。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代碼中,函數f內部return命令返回的值,會被then方法回調函數接收到。
async函數內部拋出錯誤,會致使返回的Promise對象變爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到。
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
(2)async函數返回的Promise對象,必須等到內部全部await命令的Promise對象執行完,纔會發生狀態改變。也就是說,只有async函數內部的異步
操做執行完,纔會執行then方法指定的回調函數。
下面是一個例子。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
(3)正常狀況下,await命令後面是一個Promise對象。若是不是,會被轉成一個當即resolve的Promise對象。
async function f() {
return await 123;
}
f().then(v => console.log(v))
// 123
上面代碼中,await命令的參數是數值123,它被轉成Promise對象,並當即resolve。
await命令後面的Promise對象若是變爲reject狀態,則reject的參數會被catch方法的回調函數接收到。
async function f() {
await Promise.reject('出錯了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
注意,上面代碼中,await語句前面沒有return,可是reject方法的參數依然傳入了catch方法的回調函數。這裏若是在await前面加上return,效果
是同樣的。
只要一個await語句後面的Promise變爲reject,那麼整個async函數都會中斷執行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
上面代碼中,第二個await語句是不會執行的,由於第一個await語句狀態變成了reject。
爲了不這個問題,能夠將第一個await放在try...catch結構裏面,這樣第二個await就會執行。
async function f() {
try {
await Promise.reject('出錯了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
另外一種方法是await後面的Promise對象再跟一個catch方面,處理前面可能出現的錯誤。
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出錯了
// hello world
若是有多個await命令,能夠統一放在try...catch結構中。
async function main() {
try {
var val1 = await firstStep();
var val2 = await secondStep(val1);
var val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}
(4)若是await後面的異步操做出錯,那麼等同於async函數返回的Promise對象被reject。
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了
上面代碼中,async函數f執行後,await後面的Promise對象會拋出一個錯誤對象,致使catch方法的回調函數被調用,它的參數就是拋出的錯誤對
象。具體的執行機制,能夠參考後文的「async函數的實現」。
防止出錯的方法,也是將其放在try...catch代碼塊之中。
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
} catch(e) {
}
return await('hello world');
}
17.5.3 async函數的實現
async 函數的實現,就是將 Generator 函數和自動執行器,包裝在一個函數裏。
async function fn(args){
// ...
}
// 等同於
function fn(args){
return spawn(function*() {
// ...
});
}
全部的async函數均可以寫成上面的第二種形式,其中的 spawn 函數就是自動執行器。
下面給出spawn函數的實現,基本就是前文自動執行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
async函數是很是新的語法功能,新到都不屬於 ES6,而是屬於 ES7。目前,它仍處於提案階段,可是轉碼器Babel和regenerator都已經支持,轉碼
後就能使用。
17.5.4 async 函數的用法
async函數返回一個Promise對象,能夠使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到觸發的異步操做完成,再
接着執行函數體內後面的語句。
下面是一個例子。
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
上面代碼是一個獲取股票報價的函數,函數前面的async關鍵字,代表該函數內部有異步操做。調用該函數時,會當即返回一個Promise對象。
下面的例子,指定多少毫秒後輸出一個值。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
上面代碼指定50毫秒之後,輸出"hello world"。
Async函數有多種使用形式。
// 函數聲明
async function foo() {}
// 函數表達式
const foo = async function () {};
// 對象的方法
let obj = { async foo() {} };
// 箭頭函數
const foo = async () => {};
17.5.5 注意點
第一點,await命令後面的Promise對象,運行結果多是rejected,因此最好把await命令放在try...catch代碼塊中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另外一種寫法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
};
}
第二點,多個await命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發。
let foo = await getFoo();
let bar = await getBar();
上面代碼中,getFoo和getBar是兩個獨立的異步操做(即互不依賴),被寫成繼發關係。這樣比較耗時,由於只有getFoo完成之後,纔會執
行getBar,徹底可讓它們同時觸發。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面兩種寫法,getFoo和getBar都是同時觸發,這樣就會縮短程序的執行時間。
第三點,await命令只能用在async函數之中,若是用在普通函數,就會報錯。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代碼會報錯,由於await用在普通函數之中了。可是,若是將forEach方法的參數改爲async函數,也有問題。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能獲得錯誤結果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代碼可能不會正常工做,緣由是這時三個db.post操做將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for循環。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
若是確實但願多個請求併發執行,能夠使用Promise.all方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的寫法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
ES6將await增長爲保留字。使用這個詞做爲標識符,在ES5是合法的,在ES6將拋出SyntaxError。
17.5.6 與Promise、Generator的比較
咱們經過一個例子,來看Async函數與Promise、Generator函數的區別。
假定某個DOM元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。若是當中有一個動畫出錯,就再也不往下執行,返回上一個成功執
行的動畫的返回值。
首先是Promise的寫法。
function chainAnimationsPromise(elem, animations) {
// 變量ret用來保存上一個動畫的返回值
var ret = null;
// 新建一個空的Promise
var p = Promise.resolve();
// 使用then方法,添加全部動畫
for(var anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
// 返回一個部署了錯誤捕捉機制的Promise
return p.catch(function(e) {
/* 忽略錯誤,繼續執行 */
}).then(function() {
return ret;
});
}
雖然Promise的寫法比回調函數的寫法大大改進,可是一眼看上去,代碼徹底都是Promise的API(then、catch等等),操做自己的語義反而不容易看
出來。
接着是Generator函數的寫法。
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret;
});
}
上面代碼使用Generator函數遍歷了每一個動畫,語義比Promise寫法更清晰,用戶定義的操做所有都出如今spawn函數的內部。這個寫法的問題在於,
必須有一個任務運行器,自動執行Generator函數,上面代碼的spawn函數就是自動執行器,它返回一個Promise對象,並且必須保證yield語句後面的
表達式,必須返回一個Promise。
最後是Async函數的寫法。
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret;
}
能夠看到Async函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。它將Generator寫法中的自動執行器,改在語言層面提供,不暴露給用
戶,所以代碼量最少。若是使用Generator寫法,自動執行器須要用戶本身提供。
18 Class
18.1 Class基本語法
18.1.1 概述
JavaScript語言的傳統方法是經過構造函數,定義並生成新對象。下面是一個例子。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
上面這種寫法跟傳統的面嚮對象語言(好比C++和Java)差別很大,很容易讓新學習這門語言的程序員感到困惑。
ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念,做爲對象的模板。經過class關鍵字,能夠定義類。基本上,ES6的class能夠看做
只是一個語法糖,它的絕大部分功能,ES5均可以作到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼
用ES6的「類」改寫,就是下面這樣。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代碼定義了一個「類」,能夠看到裏面有一個constructor方法,這就是構造方法,而this關鍵字則表明實例對象。也就是說,ES5的構造函
數Point,對應ES6的Point類的構造方法。
Point類除了構造方法,還定義了一個toString方法。注意,定義「類」的方法的時候,前面不須要加上function這個關鍵字,直接把函數定義放進去了
就能夠了。另外,方法之間不須要逗號分隔,加了會報錯。
ES6的類,徹底能夠看做構造函數的另外一種寫法。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代碼代表,類的數據類型就是函數,類自己就指向構造函數。
使用的時候,也是直接對類使用new命令,跟構造函數的用法徹底一致。
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
構造函數的prototype屬性,在ES6的「類」上面繼續存在。事實上,類的全部方法都定義在類的prototype屬性上面。
class Point {
constructor(){
// ...
}
toString(){
// ...
}
toValue(){
// ...
}
}
// 等同於
Point.prototype = {
toString(){},
toValue(){}
};
在類的實例上面調用方法,其實就是調用原型上的方法。
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
上面代碼中,b是B類的實例,它的constructor方法就是B類原型的constructor方法。
因爲類的方法都定義在prototype對象上面,因此類的新方法能夠添加在prototype對象上面。Object.assign方法能夠很方便地一次向類添加多個方
法。
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
prototype對象的constructor屬性,直接指向「類」的自己,這與ES5的行爲是一致的。
Point.prototype.constructor === Point // true
另外,類的內部全部定義的方法,都是不可枚舉的(non-enumerable)。
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
上面代碼中,toString方法是Point類內部定義的方法,它是不可枚舉的。這一點與ES5的行爲不一致。
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
上面代碼採用ES5的寫法,toString方法就是可枚舉的。
類的屬性名,能夠採用表達式。
let methodName = "getArea";
class Square{
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
上面代碼中,Square類的方法名getArea,是從表達式獲得的。
18.1.2 constructor方法
constructor方法是類的默認方法,經過new命令生成對象實例時,自動調用該方法。一個類必須有constructor方法,若是沒有顯式定義,一個空
的constructor方法會被默認添加。
constructor() {}
constructor方法默認返回實例對象(即this),徹底能夠指定返回另一個對象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
上面代碼中,constructor函數返回一個全新的對象,結果致使實例對象不是Foo類的實例。
類的構造函數,不使用new是無法調用的,會報錯。這是它跟普通構造函數的一個主要區別,後者不用new也能夠執行。
class Foo {
constructor() {
return Object.create(null);
}
}
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
18.1.3 類的實例對象
生成類的實例對象的寫法,與ES5徹底同樣,也是使用new命令。若是忘記加上new,像函數那樣調用Class,將會報錯。
// 報錯
var point = Point(2, 3);
// 正確
var point = new Point(2, 3);
與ES5同樣,實例的屬性除非顯式定義在其自己(即定義在this對象上),不然都是定義在原型上(即定義在class上)。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
上面代碼中,x和y都是實例對象point自身的屬性(由於定義在this變量上),因此hasOwnProperty方法返回true,而toString是原型對象的屬性
(由於定義在Point類上),因此hasOwnProperty方法返回false。這些都與ES5的行爲保持一致。
與ES5同樣,類的全部實例共享一個原型對象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
上面代碼中,p1和p2都是Point的實例,它們的原型都是Point,因此__proto__屬性是相等的。
這也意味着,能夠經過實例的__proto__屬性爲Class添加方法。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
上面代碼在p1的原型上添加了一個printName方法,因爲p1的原型就是p2的原型,所以p2也能夠調用這個方法。並且,此後新建的實例p3也能夠調用
這個方法。這意味着,使用實例的__proto__屬性改寫原型,必須至關謹慎,不推薦使用,由於這會改變Class的原始定義,影響到全部實例。
18.1.4 不存在變量提高
Class不存在變量提高(hoist),這一點與ES5徹底不一樣。
new Foo(); // ReferenceError
class Foo {}
上面代碼中,Foo類使用在前,定義在後,這樣會報錯,由於ES6不會把類的聲明提高到代碼頭部。這種規定的緣由與下文要提到的繼承有關,必須保
證子類在父類以後定義。
{
let Foo = class {};
class Bar extends Foo {
}
}
上面的代碼不會報錯,由於class繼承Foo的時候,Foo已經有定義了。可是,若是存在class的提高,上面代碼就會報錯,由於class會被提高到代碼
頭部,而let命令是不提高的,因此致使class繼承Foo的時候,Foo尚未定義。
18.1.5 Class表達式
與函數同樣,類也能夠使用表達式的形式定義。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代碼使用表達式定義了一個類。須要注意的是,這個類的名字是MyClass而不是Me,Me只在Class的內部代碼可用,指代當前類。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代碼表示,Me只在Class內部有定義。
若是類的內部沒用到的話,能夠省略Me,也就是能夠寫成下面的形式。
const MyClass = class { /* ... */ };
採用Class表達式,能夠寫出當即執行的Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('張三');
person.sayName(); // "張三"
上面代碼中,person是一個當即執行的類的實例。
18.1.6 私有方法
私有方法是常見需求,但ES6不提供,只能經過變通方法模擬實現。
一種作法是在命名上加以區別。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
上面代碼中,_bar方法前面的下劃線,表示這是一個只限於內部使用的私有方法。可是,這種命名是不保險的,在類的外部,仍是能夠調用到這個方
法。
另外一種方法就是索性將私有方法移出模塊,由於模塊內部的全部方法都是對外可見的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
上面代碼中,foo是公有方法,內部調用了bar.call(this, baz)。這使得bar實際上成爲了當前模塊的私有方法。
還有一種方法是利用Symbol值的惟一性,將私有方法的名字命名爲一個Symbol值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
上面代碼中,bar和snaf都是Symbol值,致使第三方沒法獲取到它們,所以達到了私有方法和私有屬性的效果。
18.1.7 this的指向
類的方法內部若是含有this,它默認指向類的實例。可是,必須很是當心,一旦單獨使用該方法,極可能報錯。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
上面代碼中,printName方法中的this,默認指向Logger類的實例。可是,若是將這個方法提取出來單獨使用,this會指向該方法運行時所在的環
境,由於找不到print方法而致使報錯。
一個比較簡單的解決方法是,在構造方法中綁定this,這樣就不會找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
另外一種解決方法是使用箭頭函數。
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
// ...
}
還有一種解決方法是使用Proxy,獲取方法的時候,自動綁定this。
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());
18.1.8 嚴格模式
類和模塊的內部,默認就是嚴格模式,因此不須要使用use strict指定運行模式。只要你的代碼寫在類或模塊之中,就只有嚴格模式可用。
考慮到將來全部的代碼,其實都是運行在模塊之中,因此ES6實際上把整個語言升級到了嚴格模式。
18.1.9 name屬性
因爲本質上,ES6的類只是ES5的構造函數的一層包裝,因此函數的許多特性都被Class繼承,包括name屬性。
class Point {}
Point.name // "Point"
Point.name // "Point"
name屬性老是返回緊跟在class關鍵字後面的類名。
18.2 Class的繼承
18.2.1 基本用法
Class之間能夠經過extends關鍵字實現繼承,這比ES5的經過修改原型鏈實現繼承,要清晰和方便不少。
class ColorPoint extends Point {}
上面代碼定義了一個ColorPoint類,該類經過extends關鍵字,繼承了Point類的全部屬性和方法。可是因爲沒有部署任何代碼,因此這兩個類徹底一
樣,等於複製了一個Point類。下面,咱們在ColorPoint內部加上代碼。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調用父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 調用父類的toString()
}
}
上面代碼中,constructor方法和toString方法之中,都出現了super關鍵字,它在這裏表示父類的構造函數,用來新建父類的this對象。
子類必須在constructor方法中調用super方法,不然新建實例時會報錯。這是由於子類沒有本身的this對象,而是繼承父類的this對象,而後對其進
行加工。若是不調用super方法,子類就得不到this對象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
上面代碼中,ColorPoint繼承了父類Point,可是它的構造函數沒有調用super方法,致使新建實例時報錯。
ES5的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制徹底不一樣,實質是
先創造父類的實例對象this(因此必須先調用super方法),而後再用子類的構造函數修改this。
若是子類沒有定義constructor方法,這個方法會被默認添加,代碼以下。也就是說,無論有沒有顯式定義,任何一個子類都有constructor方法。
constructor(...args) {
super(...args);
}
另外一個須要注意的地方是,在子類的構造函數中,只有調用super以後,才能夠使用this關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父
類實例加工,只有super方法才能返回父類實例。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正確
}
}
上面代碼中,子類的constructor方法沒有調用super以前,就使用this關鍵字,結果報錯,而放在super方法以後就是正確的。
下面是生成子類實例的代碼。
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
上面代碼中,實例對象cp同時是ColorPoint和Point兩個類的實例,這與ES5的行爲徹底一致。
18.2.2 類的prototype屬性和__proto__屬性
大多數瀏覽器的ES5實現之中,每個對象都有__proto__屬性,指向對應的構造函數的prototype屬性。Class做爲構造函數的語法糖,同時有
prototype屬性和__proto__屬性,所以同時存在兩條繼承鏈。
(1)子類的__proto__屬性,表示構造函數的繼承,老是指向父類。
(2)子類prototype屬性的__proto__屬性,表示方法的繼承,老是指向父類的prototype屬性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
上面代碼中,子類B的__proto__屬性指向父類A,子類B的prototype屬性的__proto__屬性指向父類A的prototype屬性。
這樣的結果是由於,類的繼承是按照下面的模式實現的。
class A {
}
class B {
}
// B的實例繼承A的實例
Object.setPrototypeOf(B.prototype, A.prototype);
// B繼承A的靜態屬性
Object.setPrototypeOf(B, A);
《對象的擴展》一章給出過Object.setPrototypeOf方法的實現。
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
所以,就獲得了上面的結果。
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同於
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同於
B.__proto__ = A;
這兩條繼承鏈,能夠這樣理解:做爲一個對象,子類(B)的原型(__proto__屬性)是父類(A);做爲一個構造函數,子類(B)的原型
(prototype屬性)是父類的實例。
Object.create(A.prototype);
// 等同於
B.prototype.__proto__ = A.prototype;
18.2.3 Extends 的繼承目標
extends關鍵字後面能夠跟多種類型的值。
class B extends A {
}
上面代碼的A,只要是一個有prototype屬性的函數,就能被B繼承。因爲函數都有prototype屬性(除了Function.prototype函數),所以A能夠是任
意函數。
下面,討論三種特殊狀況。
第一種特殊狀況,子類繼承Object類。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
這種狀況下,A其實就是構造函數Object的複製,A的實例就是Object的實例。
第二種特殊狀況,不存在任何繼承。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
這種狀況下,A做爲一個基類(即不存在任何繼承),就是一個普通函數,因此直接繼承Funciton.prototype。可是,A調用後返回一個空對象
(即Object實例),因此A.prototype.__proto__指向構造函數(Object)的prototype屬性。
第三種特殊狀況,子類繼承null。
class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
這種狀況與第二種狀況很是像。A也是一個普通函數,因此直接繼承Funciton.prototype。可是,A調用後返回的對象不繼承任何方法,因此它
的__proto__指向Function.prototype,即實質上執行了下面的代碼。
class C extends null {
constructor() { return Object.create(null); }
}
18.2.4 Object.getPrototypeOf()
Object.getPrototypeOf方法能夠用來從子類上獲取父類。
Object.getPrototypeOf(ColorPoint) === Point
// true
所以,能夠使用這個方法判斷,一個類是否繼承了另外一個類。
18.2.5 super關鍵字
super這個關鍵字,有兩種用法,含義不一樣。
(1)做爲函數調用時(即super(...args)),super表明父類的構造函數。
(2)做爲對象調用時(即super.prop或super.method()),super表明父類。注意,此時super便可以引用父類實例的屬性和方法,也能夠引用父類
的靜態方法。
class B extends A {
get m() {
return this._p * super._p;
}
set m() {
throw new Error('該屬性只讀');
}
}
上面代碼中,子類經過super關鍵字,調用父類實例的_p屬性。
因爲,對象老是繼承其餘對象的,因此能夠在任意一個對象中,使用super關鍵字。
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
18.2.6 實例的__proto__屬性
子類實例的__proto__屬性的__proto__屬性,指向父類實例的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
上面代碼中,ColorPoint繼承了Point,致使前者原型的原型是後者的原型。
所以,經過子類實例的__proto__.__proto__屬性,能夠修改父類實例的行爲。
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
上面代碼在ColorPoint的實例p2上向Point類添加方法,結果影響到了Point的實例p1。
18.3 原生構造函數的繼承
原生構造函數是指語言內置的構造函數,一般用來生成數據結構。ECMAScript的原生構造函數大體有下面這些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
之前,這些原生構造函數是沒法繼承的,好比,不能本身定義一個Array的子類。
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
上面代碼定義了一個繼承Array的MyArray類。可是,這個類的行爲與Array徹底不一致。
var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"
之因此會發生這種狀況,是由於子類沒法得到原生構造函數的內部屬性,經過Array.apply()或者分配給原型對象都不行。原生構造函數會忽
略apply方法傳入的this,也就是說,原生構造函數的this沒法綁定,致使拿不到內部屬性。
ES5是先新建子類的實例對象this,再將父類的屬性添加到子類上,因爲父類的內部屬性沒法獲取,致使沒法繼承原生的構造函數。好比,Array構造
函數有一個內部屬性[[DefineOwnProperty]],用來定義新屬性時,更新length屬性,這個內部屬性沒法在子類獲取,致使子類的length屬性行爲不正
常。
下面的例子中,咱們想讓一個普通對象繼承Error對象。
var e = {};
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []
上面代碼中,咱們想經過Error.call(e)這種寫法,讓普通對象e具備Error對象的實例屬性。可是,Error.call()徹底忽略傳入的第一個參數,而是
返回一個新對象,e自己沒有任何變化。這證實了Error.call(e)這種寫法,沒法繼承原生構造函數。
ES6容許繼承原生構造函數定義子類,由於ES6是先新建父類的實例對象this,而後再用子類的構造函數修飾this,使得父類的全部行爲均可以繼
承。下面是一個繼承Array的例子。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
上面代碼定義了一個MyArray類,繼承了Array構造函數,所以就能夠從MyArray生成數組的實例。這意味着,ES6能夠自定義原生數據結構(好比
Array、String等)的子類,這是ES5沒法作到的。
上面這個例子也說明,extends關鍵字不只能夠用來繼承類,還能夠用來繼承原生的構造函數。所以能夠在原生數據結構的基礎上,定義本身的數據結
構。下面就是定義了一個帶版本功能的數組。
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.revert();
x // [1, 2]
上面代碼中,VersionedArray結構會經過commit方法,將本身的當前狀態存入history屬性,而後經過revert方法,能夠撤銷當前版本,回到上一個
版本。除此以外,VersionedArray依然是一個數組,全部原生的數組方法均可以在它上面調用。
下面是一個自定義Error子類的例子。
class ExtendableError extends Error {
constructor(message) {
super();
this.message = message;
this.stack = (new Error()).stack;
this.name = this.constructor.name;
}
}
class MyError extends ExtendableError {
constructor(m) {
super(m);
}
}
var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
// at MyError.ExtendableError
// ...
注意,繼承Object的子類,有一個行爲差別。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
console.log(o.attr === true); // false
上面代碼中,NewObj繼承了Object,可是沒法經過super方法向父類Object傳參。這是由於ES6改變了Object構造函數的行爲,一旦發現Object方法
不是經過new Object()這種形式調用,ES6規定Object構造函數會忽略參數。
18.4 Class的取值函數(getter)和存值函數(setter)
與ES5同樣,在Class內部能夠使用get和set關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
上面代碼中,prop屬性有對應的存值函數和取值函數,所以賦值和讀取行爲都被自定義了。
存值函數和取值函數是設置在屬性的descriptor對象上的。
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.prototype, "html");
"get" in descriptor // true
"set" in descriptor // true
上面代碼中,存值函數和取值函數是定義在html屬性的描述對象上面,這與ES5徹底一致。
18.5 Class的Generator方法
若是某個方法以前加上星號(*),就表示該方法是一個Generator函數。
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
上面代碼中,Foo類的Symbol.iterator方法前有一個星號,表示該方法是一個Generator函數。Symbol.iterator方法返回一個Foo類的默認遍歷
器,for...of循環會自動調用這個遍歷器。
18.6 Class的靜態方法
類至關於實例的原型,全部在類中定義的方法,都會被實例繼承。若是在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接
經過類來調用,這就稱爲「靜態方法」。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
上面代碼中,Foo類的classMethod方法前有static關鍵字,代表該方法是一個靜態方法,能夠直接在Foo類上調用(Foo.classMethod()),而不是
在Foo類的實例上調用。若是在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。
父類的靜態方法,能夠被子類繼承。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod(); // 'hello'
上面代碼中,父類Foo有一個靜態方法,子類Bar能夠調用這個方法。
靜態方法也是能夠從super對象上調用的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod();
18.7 Class的靜態屬性和實例屬性
靜態屬性指的是Class自己的屬性,即Class.propname,而不是定義在實例對象(this)上的屬性。
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的寫法爲Foo類定義了一個靜態屬性prop。
目前,只有這種寫法可行,由於ES6明確規定,Class內部只有靜態方法,沒有靜態屬性。
// 如下兩種寫法都無效
class Foo {
// 寫法一
prop: 2
// 寫法二
static prop: 2
}
Foo.prop // undefined
ES7有一個靜態屬性的提案,目前Babel轉碼器支持。
這個提案對實例屬性和靜態屬性,都規定了新的寫法。
(1)類的實例屬性
類的實例屬性能夠用等式,寫入類的定義之中。
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
上面代碼中,myProp就是MyClass的實例屬性。在MyClass的實例上,能夠讀取這個屬性。
之前,咱們定義實例屬性,只能寫在類的constructor方法裏面。
class ReactCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
}
上面代碼中,構造方法constructor裏面,定義了this.state屬性。
有了新的寫法之後,能夠不在constructor方法裏面定義。
class ReactCounter extends React.Component {
state = {
count: 0
};
}
這種寫法比之前更清晰。
爲了可讀性的目的,對於那些在constructor裏面已經定義的實例屬性,新寫法容許直接列出。
class ReactCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
state;
}
(2)類的靜態屬性
類的靜態屬性只要在上面的實例屬性寫法前面,加上static關鍵字就能夠了。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myProp); // 42
}
}
一樣的,這個新寫法大大方便了靜態屬性的表達。
// 老寫法
class Foo {
}
Foo.prop = 1;
// 新寫法
class Foo {
static prop = 1;
}
上面代碼中,老寫法的靜態屬性定義在類的外部。整個類生成之後,再生成靜態屬性。這樣讓人很容易忽略這個靜態屬性,也不符合相關代碼應該放
在一塊兒的代碼組織原則。另外,新寫法是顯式聲明(declarative),而不是賦值處理,語義更好。
18.8 new.target屬性
new是從構造函數生成實例的命令。ES6爲new命令引入了一個new.target屬性,(在構造函數中)返回new命令做用於的那個構造函數。若是構造函數
不是經過new命令調用的,new.target會返回undefined,所以這個屬性能夠用來肯定構造函數是怎麼調用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必須使用new生成實例');
}
}
// 另外一種寫法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必須使用new生成實例');
}
}
var person = new Person('張三'); // 正確
var notAPerson = Person.call(person, '張三'); // 報錯
上面代碼確保構造函數只能經過new命令調用。
Class內部調用new.target,返回當前Class。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
var obj = new Rectangle(3, 4); // 輸出 true
須要注意的是,子類繼承父類時,new.target會返回子類。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var obj = new Square(3); // 輸出 false
上面代碼中,new.target會返回子類。
利用這個特色,能夠寫出不能獨立使用、必須繼承後才能使用的類。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本類不能實例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 報錯
var y = new Rectangle(3, 4); // 正確
上面代碼中,Shape類不能被實例化,只能用於繼承。
注意,在函數外部,使用new.target會報錯。
18.9 Mixin模式的實現
Mixin模式指的是,將多個類的接口「混入」(mix in)另外一個類。它在ES6的實現以下。
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
上面代碼的mix函數,能夠將多個對象合成爲一個類。使用的時候,只要繼承這個類便可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
19 修飾器
19.1 類的修飾
修飾器(Decorator)是一個函數,用來修改類的行爲。這是ES7的一個提案,目前Babel轉碼器已經支持。
修飾器對類的行爲的改變,是代碼編譯時發生的,而不是在運行時。這意味着,修飾器能在編譯階段運行代碼。
function testable(target) {
target.isTestable = true;
}
@testable
class MyTestableClass {}
console.log(MyTestableClass.isTestable) // true
上面代碼中,@testable就是一個修飾器。它修改了MyTestableClass這個類的行爲,爲它加上了靜態屬性isTestable。
基本上,修飾器的行爲就是下面這樣。
@decorator
class A {}
// 等同於
class A {}
A = decorator(A) || A;
也就是說,修飾器本質就是編譯時執行的函數。
修飾器函數的第一個參數,就是所要修飾的目標類。
function testable(target) {
// ...
}
上面代碼中,testable函數的參數target,就是會被修飾的類。
若是以爲一個參數不夠用,能夠在修飾器外面再封裝一層函數。
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
上面代碼中,修飾器testable能夠接受參數,這就等於能夠修改修飾器的行爲。
前面的例子是爲類添加一個靜態屬性,若是想添加實例屬性,能夠經過目標類的prototype對象操做。
function testable(target) {
target.prototype.isTestable = true;
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass();
obj.isTestable // true
上面代碼中,修飾器函數testable是在目標類的prototype對象上添加屬性,所以就能夠在實例上調用。
下面是另一個例子。
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
// main.js
import { mixins } from './mixins'
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // 'foo'
上面代碼經過修飾器mixins,把Foo類的方法添加到了MyClass的實例上面。能夠用Object.assign()模擬這個功能。
const Foo = {
foo() { console.log('foo') }
};
class MyClass {}
Object.assign(MyClass.prototype, Foo);
let obj = new MyClass();
obj.foo() // 'foo'
19.2 方法的修飾
修飾器不只能夠修飾類,還能夠修飾類的屬性。
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
上面代碼中,修飾器readonly用來修飾「類」的name方法。
此時,修飾器函數一共能夠接受三個參數,第一個參數是所要修飾的目標對象,第二個參數是所要修飾的屬性名,第三個參數是該屬性的描述對象。
function readonly(target, name, descriptor){
// descriptor對象原來的值以下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 相似於
Object.defineProperty(Person.prototype, 'name', descriptor);
上面代碼說明,修飾器(readonly)會修改屬性的描述對象(descriptor),而後被修改的描述對象再用來定義屬性。
下面是另外一個例子,修改屬性描述對象的enumerable屬性,使得該屬性不可遍歷。
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
下面的@log修飾器,能夠起到輸出日誌的做用。
class Math {
@log
add(a, b) {
return a + b;
}
}
function log(target, name, descriptor) {
var oldValue = descriptor.value;
descriptor.value = function() {
console.log(`Calling "${name}" with`, arguments);
return oldValue.apply(null, arguments);
};
return descriptor;
}
const math = new Math();
// passed parameters should get logged now
math.add(2, 4);
上面代碼中,@log修飾器的做用就是在執行原始的操做以前,執行一次console.log,從而達到輸出日誌的目的。
修飾器有註釋的做用。
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}` }
}
從上面代碼中,咱們一眼就能看出,Person類是可測試的,而name方法是隻讀和不可枚舉的。
若是同一個方法有多個修飾器,會像剝洋蔥同樣,先從外到內進入,而後由內向外執行。
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1
上面代碼中,外層修飾器@dec(1)先進入,可是內層修飾器@dec(2)先執行。
除了註釋,修飾器還能用來類型檢查。因此,對於類來講,這項功能至關有用。從長期來看,它將是JavaScript代碼靜態分析的重要工具。
19.3 爲何修飾器不能用於函數?
修飾器只能用於類和類的方法,不能用於函數,由於存在函數提高。
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
上面的代碼,意圖是執行後counter等於1,可是實際上結果是counter等於0。由於函數提高,使得實際執行的代碼是下面這樣。
var counter;
var add;
@add
function foo() {
}
counter = 0;
add = function () {
counter++;
};
下面是另外一個例子。
var readOnly = require("some-decorator");
@readOnly
function foo() {
}
上面代碼也有問題,由於實際執行是下面這樣。
var readOnly;
@readOnly
function foo() {
}
readOnly = require("some-decorator");
總之,因爲存在函數提高,使得修飾器不能用於函數。類是不會提高的,因此就沒有這方面的問題。
19.4 core-decorators.js
core-decorators.js是一個第三方模塊,提供了幾個常見的修飾器,經過它能夠更好地理解修飾器。
(1)@autobind
autobind修飾器使得方法中的this對象,綁定原始對象。
import { autobind } from 'core-decorators';
class Person {
@autobind
getPerson() {
return this;
}
}
let person = new Person();
let getPerson = person.getPerson;
getPerson() === person;
// true
(2)@readonly
readonly修飾器使得屬性或方法不可寫。
import { readonly } from 'core-decorators';
class Meal {
@readonly
entree = 'steak';
}
var dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [object Object]
(3)@override
override修飾器檢查子類的方法,是否正確覆蓋了父類的同名方法,若是不正確會報錯。
import { override } from 'core-decorators';
class Parent {
speak(first, second) {}
}
class Child extends Parent {
@override
speak() {}
// SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}
// or
class Child extends Parent {
@override
speaks() {}
// SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
//
// Did you mean "speak"?
}
(4)@deprecate (別名@deprecated)
deprecate或deprecated修飾器在控制檯顯示一條警告,表示該方法將廢除。
import { deprecate } from 'core-decorators';
class Person {
@deprecate
facepalm() {}
@deprecate('We stopped facepalming')
facepalmHard() {}
@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
facepalmHarder() {}
}
let person = new Person();
person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.
person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming
person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
// See http://knowyourmeme.com/memes/facepalm for more details.
//
(5)@suppressWarnings
suppressWarnings修飾器抑制decorated修飾器致使的console.warn()調用。可是,異步代碼發出的調用除外。
import { suppressWarnings } from 'core-decorators';
class Person {
@deprecated
facepalm() {}
@suppressWarnings
facepalmWithoutWarning() {
this.facepalm();
}
}
let person = new Person();
person.facepalmWithoutWarning();
// no warning is logged
19.5 使用修飾器實現自動發佈事件
咱們能夠使用修飾器,使得對象的方法被調用時,自動發出一個事件。
import postal from "postal/lib/postal.lodash";
export default function publish(topic, channel) {
return function(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function() {
let value = fn.apply(this, arguments);
postal.channel(channel || target.channel || "/").publish(topic, value);
};
};
}
上面代碼定義了一個名爲publish的修飾器,它經過改寫descriptor.value,使得原方法被調用時,會自動發出一個事件。它使用的事件「發佈/訂閱」庫
是Postal.js。
它的用法以下。
import publish from "path/to/decorators/publish";
class FooComponent {
@publish("foo.some.message", "component")
someMethod() {
return {
my: "data"
};
}
@publish("foo.some.other")
anotherMethod() {
// ...
}
}
之後,只要調用someMethod或者anotherMethod,就會自動發出一個事件。
let foo = new FooComponent();
foo.someMethod() // 在"component"頻道發佈"foo.some.message"事件,附帶的數據是{ my: "data" }
foo.anotherMethod() // 在"/"頻道發佈"foo.some.other"事件,不附帶數據
19.6 Mixin
在修飾器的基礎上,能夠實現Mixin模式。所謂Mixin模式,就是對象繼承的一種替代方案,中文譯爲「混入」(mix in),意爲在一個對象之中混入另外
一個對象的方法。
請看下面的例子。
const Foo = {
foo() { console.log('foo') }
};
class MyClass {}
Object.assign(MyClass.prototype, Foo);
let obj = new MyClass();
obj.foo() // 'foo'
上面代碼之中,對象Foo有一個foo方法,經過Object.assign方法,能夠將foo方法「混入」MyClass類,致使MyClass的實例obj對象都具備foo方法。這
就是「混入」模式的一個簡單實現。
下面,咱們部署一個通用腳本mixins.js,將mixin寫成一個修飾器。
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}
而後,就能夠使用上面這個修飾器,爲類「混入」各類方法。
import { mixins } from './mixins';
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // "foo"
經過mixins這個修飾器,實現了在MyClass類上面「混入」Foo對象的foo方法。
不過,上面的方法會改寫MyClass類的prototype對象,若是不喜歡這一點,也能夠經過類的繼承實現mixin。
class MyClass extends MyBaseClass {
/* ... */
}
上面代碼中,MyClass繼承了MyBaseClass。若是咱們想在MyClass裏面「混入」一個foo方法,一個辦法是在MyClass和MyBaseClass之間插入一個混入
類,這個類具備foo方法,而且繼承了MyBaseClass的全部方法,而後MyClass再繼承這個類。
let MyMixin = (superclass) => class extends superclass {
foo() {
console.log('foo from MyMixin');
}
};
上面代碼中,MyMixin是一個混入類生成器,接受superclass做爲參數,而後返回一個繼承superclass的子類,該子類包含一個foo方法。
接着,目標類再去繼承這個混入類,就達到了「混入」foo方法的目的。
class MyClass extends MyMixin(MyBaseClass) {
/* ... */
}
let c = new MyClass();
c.foo(); // "foo from MyMixin"
若是須要「混入」多個方法,就生成多個混入類。
class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
/* ... */
}
這種寫法的一個好處,是能夠調用super,所以能夠避免在「混入」過程當中覆蓋父類的同名方法。
let Mixin1 = (superclass) => class extends superclass {
foo() {
console.log('foo from Mixin1');
if (super.foo) super.foo();
}
};
let Mixin2 = (superclass) => class extends superclass {
foo() {
console.log('foo from Mixin2');
if (super.foo) super.foo();
}
};
class S {
foo() {
console.log('foo from S');
}
}
class C extends Mixin1(Mixin2(S)) {
foo() {
console.log('foo from C');
super.foo();
}
}
上面代碼中,每一次混入發生時,都調用了父類的super.foo方法,致使父類的同名方法沒有被覆蓋,行爲被保留了下來。
new C().foo()
// foo from C
// foo from Mixin1
// foo from Mixin2
// foo from S
19.7 Trait
Trait也是一種修飾器,效果與Mixin相似,可是提供更多功能,好比防止同名方法的衝突、排除混入某些方法、爲混入的方法起別名等等。
下面採用traits-decorator這個第三方模塊做爲例子。這個模塊提供的traits修飾器,不只能夠接受對象,還能夠接受ES6類做爲參數。
import { traits } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') }
};
@traits(TFoo, TBar)
class MyClass { }
let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar
上面代碼中,經過traits修飾器,在MyClass類上面「混入」了TFoo類的foo方法和TBar對象的bar方法。
Trait不容許「混入」同名方法。
import { traits } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') },
foo() { console.log('foo') }
};
@traits(TFoo, TBar)
class MyClass { }
// 報錯
// throw new Error('Method named: ' + methodName + ' is defined twice.');
// ^
// Error: Method named: foo is defined twice.
上面代碼中,TFoo和TBar都有foo方法,結果traits修飾器報錯。
一種解決方法是排除TBar的foo方法。
import { traits, excludes } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') },
foo() { console.log('foo') }
};
@traits(TFoo, TBar::excludes('foo'))
class MyClass { }
let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar
上面代碼使用綁定運算符(::)在TBar上排除foo方法,混入時就不會報錯了。
另外一種方法是爲TBar的foo方法起一個別名。
import { traits, alias } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') },
foo() { console.log('foo') }
};
@traits(TFoo, TBar::alias({foo: 'aliasFoo'}))
class MyClass { }
let obj = new MyClass();
obj.foo() // foo
obj.aliasFoo() // foo
obj.bar() // bar
上面代碼爲TBar的foo方法起了別名aliasFoo,因而MyClass也能夠混入TBar的foo方法了。
alias和excludes方法,能夠結合起來使用。
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
class MyClass {}
上面代碼排除了TExample的foo方法和bar方法,爲baz方法起了別名exampleBaz。
as方法則爲上面的代碼提供了另外一種寫法。
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
class MyClass {}
19.8 Babel轉碼器的支持
目前,Babel轉碼器已經支持Decorator。
首先,安裝babel-core和babel-plugin-transform-decorators。因爲後者包括在babel-preset-stage-0之中,因此改成安
裝babel-preset-stage-0亦可。
$ npm install babel-core babel-plugin-transform-decorators
而後,設置配置文件.babelrc。
{
"plugins": ["transform-decorators"]
}
這時,Babel就能夠對Decorator轉碼了。
腳本中打開的命令以下。
babel.transform("code", {plugins: ["transform-decorators"]})
Babel的官方網站提供一個在線轉碼器,只要勾選Experimental,就能支持Decorator的在線轉碼。
20 Module
ES6的Class只是面向對象編程的語法糖,升級了ES5的構造函數的原型鏈繼承的寫法,並無解決模塊化問題。Module功能就是爲了解決這個問題而
提出的。
歷史上,JavaScript一直沒有模塊(module)體系,沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。其餘語言都有這項功能,
好比Ruby的require、Python的import,甚至就連CSS都有@import,可是JavaScript任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了
巨大障礙。
在ES6以前,社區制定了一些模塊加載方案,最主要的有CommonJS和AMD兩種。前者用於服務器,後者用於瀏覽器。ES6在語言規格的層面上,實現
了模塊功能,並且實現得至關簡單,徹底能夠取代現有的CommonJS和AMD規範,成爲瀏覽器和服務器通用的模塊解決方案。
ES6模塊的設計思想,是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時確
定這些東西。好比,CommonJS模塊就是對象,輸入時必須查找對象屬性。
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// 等同於
let _fs = require('fs');
let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;
上面代碼的實質是總體加載fs模塊(即加載fs的全部方法),生成一個對象(_fs),而後再從這個對象上面讀取3個方法。這種加載稱爲「運行時加
載」,由於只有運行時才能獲得這個對象,致使徹底沒辦法在編譯時作「靜態優化」。
ES6模塊不是對象,而是經過export命令顯式指定輸出的代碼,輸入時也採用靜態命令的形式。
// ES6模塊
import { stat, exists, readFile } from 'fs';
上面代碼的實質是從fs模塊加載3個方法,其餘方法不加載。這種加載稱爲「編譯時加載」,即ES6能夠在編譯時就完成模塊加載,效率要比CommonJS
模塊的加載方式高。固然,這也致使了無法引用ES6模塊自己,由於它不是對象。
因爲ES6模塊是編譯時加載,使得靜態分析成爲可能。有了它,就能進一步拓寬JavaScript的語法,好比引入宏(macro)和類型檢驗(type system)
這些只能靠靜態分析實現的功能。
除了靜態加載帶來的各類好處,ES6模塊還有如下好處。
再也不須要UMD模塊格式了,未來服務器和瀏覽器都會支持ES6模塊格式。目前,經過各類工具庫,其實已經作到了這一點。
未來瀏覽器的新API就能用模塊格式提供,再也不必要作成全局變量或者navigator對象的屬性。
再也不須要對象做爲命名空間(好比Math對象),將來這些功能能夠經過模塊提供。
瀏覽器使用ES6模塊的語法以下。
<script type="module" src="foo.js"></script>
上面代碼在網頁中插入一個模塊foo.js,因爲type屬性設爲module,因此瀏覽器知道這是一個ES6模塊。
Node的默認模塊格式是CommonJS,目前還沒決定怎麼支持ES6模塊。因此,只能經過Babel這樣的轉碼器,在Node裏面使用ES6模塊。
20.1 嚴格模式
ES6的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";。
嚴格模式主要有如下限制。
變量必須聲明後再使用
函數的參數不能有同名屬性,不然報錯
不能使用with語句
不能對只讀屬性賦值,不然報錯
不能使用前綴0表示八進制數,不然報錯
不能刪除不可刪除的屬性,不然報錯
不能刪除變量delete prop,會報錯,只能刪除屬性delete global[prop]
eval不會在它的外層做用域引入變量
eval和arguments不能被從新賦值
arguments不會自動反映函數參數的變化
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局對象
不能使用fn.caller和fn.arguments獲取函數調用的堆棧
增長了保留字(好比protected、static和interface)
上面這些限制,模塊都必須遵照。因爲嚴格模式是ES5引入的,不屬於ES6,因此請參閱相關ES5書籍,本書再也不詳細介紹了。
20.2 export命令
模塊功能主要由兩個命令構成:export和import。export命令用於規定模塊的對外接口,import命令用於輸入其餘模塊提供的功能。
一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是你但願外部可以讀取模塊內部的某個變量,就必須使用export關鍵字輸
出該變量。下面是一個JS文件,裏面使用export命令輸出變量。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
上面代碼是profile.js文件,保存了用戶信息。ES6將其視爲一個模塊,裏面用export命令對外部輸出了三個變量。
export的寫法,除了像上面這樣,還有另一種。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
上面代碼在export命令後面,使用大括號指定所要輸出的一組變量。它與前一種寫法(直接放置在var語句前)是等價的,可是應該優先考慮使用這種
寫法。由於這樣就能夠在腳本尾部,一眼看清楚輸出了哪些變量。
export命令除了輸出變量,還能夠輸出函數或類(class)。
export function multiply(x, y) {
return x * y;
};
上面代碼對外輸出一個函數multiply。
一般狀況下,export輸出的變量就是原本的名字,可是能夠使用as關鍵字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
上面代碼使用as關鍵字,重命名了函數v1和v2的對外接口。重命名後,v2能夠用不一樣的名字輸出兩次。
須要特別注意的是,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 報錯
export 1;
// 報錯
var m = 1;
export m;
上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出1,第二種寫法經過變量m,仍是直接輸出1。1只是一個值,不是接口。正確
的寫法是下面這樣。
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var n = 1;
export {n as m};
上面三種寫法都是正確的,規定了對外的接口m。其餘腳本能夠經過這個接口,取到值1。它們的實質是,在接口名與模塊內部變量之間,創建了一一
對應的關係。
一樣的,function和class的輸出,也必須遵照這樣的寫法。
// 報錯
function f() {}
export f;
// 正確
export function f() {};
// 正確
function f() {}
export {f};
另外,export語句輸出的接口,與其對應的值是動態綁定關係,即經過該接口,能夠取到模塊內部實時的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代碼輸出變量foo,值爲bar,500毫秒以後變成baz。
這一點與CommonJS規範徹底不一樣。CommonJS模塊輸出的是值的緩存,不存在動態更新,詳見下文《ES6模塊加載的實質》一節。
最後,export命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,下一節的import命令也是如此。這是
由於處於條件代碼塊之中,就無法作靜態優化了,違背了ES6模塊的設計初衷。
function foo() {
export default 'bar' // SyntaxError
}
foo()
上面代碼中,export語句放在函數之中,結果報錯。
20.3 import命令
使用export命令定義了模塊的對外接口之後,其餘JS文件就能夠經過import命令加載這個模塊(文件)。
// main.js
import {firstName, lastName, year} from './profile';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
上面代碼的import命令,就用於加載profile.js文件,並從中輸入變量。import命令接受一個對象(用大括號表示),裏面指定要從其餘模塊導入的
變量名。大括號裏面的變量名,必須與被導入模塊(profile.js)對外接口的名稱相同。
若是想爲輸入的變量從新取一個名字,import命令要使用as關鍵字,將輸入的變量重命名。
import { lastName as surname } from './profile';
注意,import命令具備提高效果,會提高到整個模塊的頭部,首先執行。
foo();
import { foo } from 'my_module';
上面的代碼不會報錯,由於import的執行早於foo的調用。
若是在一個模塊之中,先輸入後輸出同一個模塊,import語句能夠與export語句寫在一塊兒。
export { es6 as default } from './someModule';
// 等同於
import { es6 } from './someModule';
export default es6;
上面代碼中,export和import語句能夠結合在一塊兒,寫成一行。可是從可讀性考慮,不建議採用這種寫法,而應該採用標準寫法。
另外,ES7有一個提案,簡化先輸入後輸出的寫法,拿掉輸出時的大括號。
// 提案的寫法
export v from 'mod';
// 現行的寫法
export {v} from 'mod';
import語句會執行所加載的模塊,所以能夠有下面的寫法。
import 'lodash';
上面代碼僅僅執行lodash模塊,可是不輸入任何值。
20.4 模塊的總體加載
除了指定加載某個輸出值,還能夠使用總體加載,即用星號(*)指定一個對象,全部輸出值都加載在這個對象上面。
下面是一個circle.js文件,它輸出兩個方法area和circumference。
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
如今,加載這個模塊。
// main.js
import { area, circumference } from './circle';
console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));
上面寫法是逐一指定要加載的方法,總體加載的寫法以下。
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
20.5 export default命令
從前面的例子能夠看出,使用import命令的時候,用戶須要知道所要加載的變量名或函數名,不然沒法加載。可是,用戶確定但願快速上手,未必願
意閱讀文檔,去了解模塊有哪些屬性和方法。
爲了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default命令,爲模塊指定默認輸出。
// export-default.js
export default function () {
console.log('foo');
}
上面代碼是一個模塊文件export-default.js,它的默認輸出是一個函數。
其餘模塊加載該模塊時,import命令能夠爲該匿名函數指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代碼的import命令,能夠用任意名稱指向export-default.js輸出的方法,這時就不須要知道原模塊輸出的函數名。須要注意的是,這時import命
令後面,不使用大括號。
export default命令用在非匿名函數前,也是能夠的。
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者寫成
function foo() {
console.log('foo');
}
export default foo;
上面代碼中,foo函數的函數名foo,在模塊外部是無效的。加載的時候,視同匿名函數加載。
下面比較一下默認輸出和正常輸出。
// 輸出
export default function crc32() {
// ...
}
// 輸入
import crc32 from 'crc32';
// 輸出
export function crc32() {
// ...
};
// 輸入
import {crc32} from 'crc32';
上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不須要使用大括號;第二組是不使用export default時,對應的import語
句須要使用大括號。
export default命令用於指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,所以export deault命令只能使用一次。因此,import命令後面
纔不用加大括號,由於只可能對應一個方法。
本質上,export default就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。因此,下面的寫法是有效的。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同於
// export default add;
// app.js
import { default as xxx } from 'modules';
// 等同於
// import xxx from 'modules';
正是由於export default命令其實只是輸出一個叫作default的變量,因此它後面不能跟變量聲明語句。
// 正確
export var a = 1;
// 正確
var a = 1;
export default a;
// 錯誤
export default var a = 1;
上面代碼中,export default a的含義是將變量a的值賦給變量default。因此,最後一種寫法會報錯。
有了export default命令,輸入模塊時就很是直觀了,以輸入jQuery模塊爲例。
import $ from 'jquery';
若是想在一條import語句中,同時輸入默認方法和其餘變量,能夠寫成下面這樣。
import customName, { otherMethod } from './export-default';
若是要輸出默認的值,只需將值跟在export default以後便可。
export default 42;
export default也能夠用來輸出類。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
20.6 模塊的繼承
模塊之間也能夠繼承。
假設有一個circleplus模塊,繼承了circle模塊。
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
上面代碼中的export *,表示再輸出circle模塊的全部屬性和方法。注意,export *命令會忽略circle模塊的default方法。而後,上面代碼又輸出
了自定義的e變量和默認方法。
這時,也能夠將circle的屬性或方法,更名後再輸出。
// circleplus.js
export { area as circleArea } from 'circle';
上面代碼表示,只輸出circle模塊的area方法,且將其更名爲circleArea。
加載上面模塊的寫法以下。
// main.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
上面代碼中的import exp表示,將circleplus模塊的默認方法加載爲exp方法。
20.7 ES6模塊加載的實質
ES6模塊加載的機制,與CommonJS模塊徹底不一樣。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。
CommonJS模塊輸出的是被輸出值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js的例子。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。而後,在main.js裏面加載這個模塊。
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代碼說明,lib.js模塊加載之後,它的內部變化就影響不到輸出的mod.counter了。這是由於mod.counter是一個原始類型的值,會被緩存。除非
寫成一個函數,才能獲得內部變更後的值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
上面代碼中,輸出的counter屬性其實是一個取值器函數。如今再執行main.js,就能夠正確讀取內部變量counter的變更了。
$ node main.js
3
4
ES6模塊的運行機制與CommonJS不同,它遇到模塊加載命令import時,不會去執行模塊,而是隻生成一個動態的只讀引用。等到真的須要用到
時,再到模塊裏面去取值,換句話說,ES6的輸入有點像Unix系統的「符號鏈接」,原始值變了,import輸入的值也會跟着變。所以,ES6模塊是動態引
用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。
仍是舉上面的例子。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
上面代碼說明,ES6模塊輸入的變量counter是活的,徹底反應其所在模塊lib.js內部的變化。
再舉一個出如今export一節中的例子。
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
上面代碼中,m1.js的變量foo,在剛加載時等於bar,過了500毫秒,又變爲等於baz。
讓咱們看看,m2.js可否正確讀取這個變化。
$ babel-node m2.js
bar
baz
上面代碼代表,ES6模塊不會緩存運行結果,而是動態地去被加載的模塊取值,而且變量老是綁定其所在的模塊。
因爲ES6輸入的模塊變量,只是一個「符號鏈接」,因此這個變量是隻讀的,對它進行從新賦值會報錯。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
上面代碼中,main.js從lib.js輸入變量obj,能夠對obj添加屬性,可是從新賦值就會報錯。由於變量obj指向的地址是隻讀的,不能從新賦值,這就
比如main.js創造了一個名爲obj的const變量。
最後,export經過接口,輸出的是同一個值。不一樣的腳本加載這個接口,獲得的都是一樣的實例。
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
上面的腳本mod.js,輸出的是一個C的實例。不一樣的腳本加載這個模塊,獲得的都是同一個實例。
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
如今執行main.js,輸出的是1。
$ babel-node main.js
1
這就證實了x.js和y.js加載的都是C的同一個實例。
20.8 循環加載
「循環加載」(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。
// a.js
var b = require('b');
// b.js
var a = require('a');
一般,「循環加載」表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現。
可是實際上,這是很難避免的,尤爲是依賴關係複雜的大項目,很容易出現a依賴b,b依賴c,c又依賴a這樣的狀況。這意味着,模塊加載機制必須考
慮「循環加載」的狀況。
對於JavaScript語言來講,目前最多見的兩種模塊格式CommonJS和ES6,處理「循環加載」的方法是不同的,返回的結果也不同。
20.8.1 CommonJS模塊的加載原理
介紹ES6如何處理"循環加載"以前,先介紹目前最流行的CommonJS模塊格式的加載原理。
CommonJS的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。
{
id: '...',
exports: { ... },
loaded: true,
...
}
上面代碼就是Node內部加載模塊後生成的一個對象。該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表
示該模塊的腳本是否執行完畢。其餘還有不少屬性,這裏都省略了。
之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是
說,CommonJS模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。
20.8.2 CommonJS模塊的循環加載
CommonJS模塊的重要特性是加載時執行,即腳本代碼在require的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部
分,還未執行的部分不會輸出。
讓咱們來看,Node官方文檔裏面的例子。腳本文件a.js代碼以下。
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');
上面代碼之中,a.js腳本先輸出一個done變量,而後加載另外一個腳本文件b.js。注意,此時a.js代碼就停在這裏,等待b.js執行完畢,再往下執行。
再看b.js的代碼。
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');
上面代碼之中,b.js執行到第二行,就會去加載a.js,這時,就發生了「循環加載」。系統會去a.js模塊對應對象的exports屬性取值,但是由於a.js還
沒有執行完,從exports屬性只能取回已經執行的部分,而不是最後的值。
a.js已經執行的部分,只有一行。
exports.done = false;
所以,對於b.js來講,它從a.js只輸入一個變量done,值爲false。
而後,b.js接着往下執行,等到所有執行完畢,再把執行權交還給a.js。因而,a.js接着往下執行,直到執行完畢。咱們寫一個腳本main.js,驗證
這個過程。
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
執行main.js,運行結果以下。
$ node main.js
在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true
上面的代碼證實了兩件事。一是,在b.js之中,a.js沒有執行完畢,只執行了第一行。二是,main.js執行到第二行時,不會再次執行b.js,而是輸
出緩存的b.js的執行結果,即它的第四行。
exports.done = true;
總之,CommonJS輸入的是被輸出值的拷貝,不是引用。
另外,因爲CommonJS模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼所有執行後的值,二者可能會有差別。因此,輸入變量
的時候,必須很是當心。
var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險的寫法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一個部分加載時的值
};
上面代碼中,若是發生循環加載,require('a').foo的值極可能後面會被改寫,改用require('a')會更保險一點。
20.8.3 ES6模塊的循環加載
ES6處理「循環加載」與CommonJS有本質的不一樣。ES6模塊是動態引用,若是使用import從一個模塊加載變量(即import foo from 'foo'),那些變
量不會被緩存,而是成爲一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。
請看下面這個例子。
// a.js以下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';
// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';
上面代碼中,a.js加載b.js,b.js又加載a.js,構成循環加載。執行a.js,結果以下。
$ babel-node a.js
b.js
undefined
a.js
bar
上面代碼中,因爲a.js的第一行是加載b.js,因此先執行的是b.js。而b.js的第一行又是加載a.js,這時因爲a.js已經開始執行了,因此不會重複執
行,而是繼續往下執行b.js,因此第一行輸出的是b.js。
接着,b.js要打印變量foo,這時a.js還沒執行完,取不到foo的值,致使打印出來是undefined。b.js執行完,開始執行a.js,這時就一切正常了。
再看一個稍微複雜的例子(摘自 Dr. Axel Rauschmayer 的《Exploring ES6》)。
// a.js
import {bar} from './b.js';
export function foo() {
console.log('foo');
bar();
console.log('執行完畢');
}
foo();
// b.js
import {foo} from './a.js';
export function bar() {
console.log('bar');
if (Math.random() > 0.5) {
foo();
}
}
按照CommonJS規範,上面的代碼是無法執行的。a先加載b,而後b又加載a,這時a尚未任何執行結果,因此輸出結果爲null,即對於b.js來講,
變量foo的值等於null,後面的foo()就會報錯。
可是,ES6能夠執行上面的代碼。
$ babel-node a.js
foo
bar
執行完畢
// 執行結果也有多是
foo
bar
foo
bar
執行完畢
執行完畢
上面代碼中,a.js之因此可以執行,緣由就在於ES6加載的變量,都是動態引用其所在的模塊。只要引用存在,代碼就能執行。
下面,咱們詳細分析這段代碼的運行過程。
// a.js
// 這一行創建一個引用,
// 從`b.js`引用`bar`
import {bar} from './b.js';
export function foo() {
// 執行時第一行輸出 foo
console.log('foo');
// 到 b.js 執行 bar
bar();
console.log('執行完畢');
}
foo();
// b.js
// 創建`a.js`的`foo`引用
import {foo} from './a.js';
export function bar() {
// 執行時,第二行輸出 bar
console.log('bar');
// 遞歸執行 foo,一旦隨機數
// 小於等於0.5,就中止執行
if (Math.random() > 0.5) {
foo();
}
}
咱們再來看ES6模塊加載器SystemJS給出的一個例子。
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n != 0 && even(n - 1);
}
上面代碼中,even.js裏面的函數even有一個參數n,只要不等於0,就會減去1,傳入加載的odd()。odd.js也會作相似操做。
運行上面這段代碼,結果以下。
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
上面代碼中,參數n從10變爲0的過程當中,even()一共會執行6次,因此變量counter等於6。第二次調用even()時,參數n從20變爲0,even()一共會執
行11次,加上前面的6次,因此變量counter等於17。
這個例子要是改寫成CommonJS,就根本沒法執行,會報錯。
// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function(n) {
return n != 0 && even(n - 1);
}
上面代碼中,even.js加載odd.js,而odd.js又去加載even.js,造成「循環加載」。這時,執行引擎就會輸出even.js已經執行的部分(不存在任何結
果),因此在odd.js之中,變量even等於null,等到後面調用even(n-1)就會報錯。
$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function
20.9 跨模塊常量
上面說過,const聲明的常量只在當前代碼塊有效。若是想設置跨模塊的常量(即跨多個文件),能夠採用下面的寫法。
// constants.js 模塊
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模塊
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模塊
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
20.10 ES6模塊的轉碼
瀏覽器目前還不支持ES6模塊,爲了如今就能使用,能夠將轉爲ES5的寫法。除了Babel能夠用來轉碼以外,還有如下兩個方法,也能夠用來轉碼。
20.10.1 ES6 module transpiler
ES6 module transpiler是square公司開源的一個轉碼器,能夠將ES6模塊轉爲CommonJS模塊或AMD模塊的寫法,從而在瀏覽器中使用。
首先,安裝這個轉瑪器。
$ npm install -g es6-module-transpiler
而後,使用compile-modules convert命令,將ES6模塊文件轉碼。
$ compile-modules convert file1.js file2.js
-o參數能夠指定轉碼後的文件名。
$ compile-modules convert -o out.js file1.js
20.10.2 SystemJS
另外一種解決方法是使用SystemJS。它是一個墊片庫(polyfill),能夠在瀏覽器內加載ES6模塊、AMD模塊和CommonJS模塊,將其轉爲ES5格式。它
在後臺調用的是Google的Traceur轉碼器。
使用時,先在網頁內載入system.js文件。
<script src="system.js"></script>
而後,使用System.import方法加載模塊文件。
<script>
System.import('./app.js');
</script>
上面代碼中的./app,指的是當前目錄下的app.js文件。它能夠是ES6模塊文件,System.import會自動將其轉碼。
須要注意的是,System.import使用異步加載,返回一個Promise對象,能夠針對這個對象編程。下面是一個模塊文件。
// app/es6-file.js:
export class q {
constructor() {
this.es6 = 'hello';
}
}
而後,在網頁內加載這個模塊文件。
<script>
System.import('app/es6-file').then(function(m) {
console.log(new m.q().es6); // hello
});
</script>
上面代碼中,System.import方法返回的是一個Promise對象,因此能夠用then方法指定回調函數。
21 編程風格
本章探討如何將ES6的新語法,運用到編碼實踐之中,與傳統的JavaScript語法結合在一塊兒,寫出合理的、易於閱讀和維護的代碼。
多家公司和組織已經公開了它們的風格規範,具體可參閱jscs.info,下面的內容主要參考了Airbnb的JavaScript風格規範。
21.1 塊級做用域
(1)let取代var
ES6提出了兩個新的聲明變量的命令:let和const。其中,let徹底能夠取代var,由於二者語義相同,並且let沒有反作用。
'use strict';
if (true) {
let x = 'hello';
}
for (let i = 0; i < 10; i++) {
console.log(i);
}
上面代碼若是用var替代let,實際上就聲明瞭兩個全局變量,這顯然不是本意。變量應該只在其聲明的代碼塊內有效,var命令作不到這一點。
var命令存在變量提高效用,let命令沒有這個問題。
'use strict';
if(true) {
console.log(x); // ReferenceError
let x = 'hello';
}
上面代碼若是使用var替代let,console.log那一行就不會報錯,而是會輸出undefined,由於變量聲明提高到代碼塊的頭部。這違反了變量先聲明後
使用的原則。
因此,建議再也不使用var命令,而是使用let命令取代。
(2)全局常量和線程安全
在let和const之間,建議優先使用const,尤爲是在全局環境,不該該設置變量,只應設置常量。這符合函數式編程思想,有利於未來的分佈式運算。
// bad
var a = 1, b = 2, c = 3;
// good
const a = 1;
const b = 2;
const c = 3;
// best
const [a, b, c] = [1, 2, 3];
const聲明常量還有兩個好處,一是閱讀代碼的人馬上會意識到不該該修改這個值,二是防止了無心間修改變量值所致使的錯誤。
全部的函數都應該設置爲常量。
長遠來看,JavaScript可能會有多線程的實現(好比Intel的River Trail那一類的項目),這時let表示的變量,只應出如今單線程運行的代碼中,不能是
多線程共享的,這樣有利於保證線程安全。
21.2 字符串
靜態字符串一概使用單引號或反引號,不使用雙引號。動態字符串使用反引號。
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';
// acceptable
const c = `foobar`;
// good
const a = 'foobar';
const b = `foo${a}bar`;
const c = 'foobar';
21.3 解構賦值
使用數組成員對變量賦值時,優先使用解構賦值。
const arr = [1, 2, 3, 4];
// bad
const first = arr[0];
const second = arr[1];
// good
const [first, second] = arr;
函數的參數若是是對象的成員,優先使用解構賦值。
// bad
function getFullName(user) {
const firstName = user.firstName;
const lastName = user.lastName;
}
// good
function getFullName(obj) {
const { firstName, lastName } = obj;
}
// best
function getFullName({ firstName, lastName }) {
}
若是函數返回多個值,優先使用對象的解構賦值,而不是數組的解構賦值。這樣便於之後添加返回值,以及更改返回值的順序。
// bad
function processInput(input) {
return [left, right, top, bottom];
}
// good
function processInput(input) {
return { left, right, top, bottom };
}
const { left, right } = processInput(input);
21.4 對象
單行定義的對象,最後一個成員不以逗號結尾。多行定義的對象,最後一個成員以逗號結尾。
// bad
const a = { k1: v1, k2: v2, };
const b = {
k1: v1,
k2: v2
};
// good
const a = { k1: v1, k2: v2 };
const b = {
k1: v1,
k2: v2,
};
對象儘可能靜態化,一旦定義,就不得隨意添加新的屬性。若是添加屬性不可避免,要使用Object.assign方法。
// bad
const a = {};
a.x = 3;
// if reshape unavoidable
const a = {};
Object.assign(a, { x: 3 });
// good
const a = { x: null };
a.x = 3;
若是對象的屬性名是動態的,能夠在創造對象的時候,使用屬性表達式定義。
// bad
const obj = {
id: 5,
name: 'San Francisco',
};
obj[getKey('enabled')] = true;
// good
const obj = {
id: 5,
name: 'San Francisco',
[getKey('enabled')]: true,
};
上面代碼中,對象obj的最後一個屬性名,須要計算獲得。這時最好採用屬性表達式,在新建obj的時候,將該屬性與其餘屬性定義在一塊兒。這樣一
來,全部屬性就在一個地方定義了。
另外,對象的屬性和方法,儘可能採用簡潔表達法,這樣易於描述和書寫。
var ref = 'some value';
// bad
const atom = {
ref: ref,
value: 1,
addValue: function (value) {
return atom.value + value;
},
};
// good
const atom = {
ref,
value: 1,
addValue(value) {
return atom.value + value;
},
};
21.5 數組
使用擴展運算符(...)拷貝數組。
// bad
const len = items.length;
const itemsCopy = [];
let i;
for (i = 0; i < len; i++) {
itemsCopy[i] = items[i];
}
// good
const itemsCopy = [...items];
使用Array.from方法,將相似數組的對象轉爲數組。
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
21.6 函數
當即執行函數能夠寫成箭頭函數的形式。
(() => {
console.log('Welcome to the Internet.');
})();
那些須要使用函數表達式的場合,儘可能用箭頭函數代替。由於這樣更簡潔,並且綁定了this。
// bad
[1, 2, 3].map(function (x) {
return x * x;
});
// good
[1, 2, 3].map((x) => {
return x * x;
});
// best
[1, 2, 3].map(x => x * x);
箭頭函數取代Function.prototype.bind,不該再用self/_this/that綁定 this。
// bad
const self = this;
const boundMethod = function(...params) {
return method.apply(self, params);
}
// acceptable
const boundMethod = method.bind(this);
// best
const boundMethod = (...params) => method.apply(this, params);
簡單的、單行的、不會複用的函數,建議採用箭頭函數。若是函數體較爲複雜,行數較多,仍是應該採用傳統的函數寫法。
全部配置項都應該集中在一個對象,放在最後一個參數,布爾值不能夠直接做爲參數。
// bad
function divide(a, b, option = false ) {
}
// good
function divide(a, b, { option = false } = {}) {
}
不要在函數體內使用arguments變量,使用rest運算符(...)代替。由於rest運算符顯式代表你想要獲取參數,並且arguments是一個相似數組的對象,
而rest運算符能夠提供一個真正的數組。
// bad
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// good
function concatenateAll(...args) {
return args.join('');
}
使用默認值語法設置函數參數的默認值。
// bad
function handleThings(opts) {
opts = opts || {};
}
// good
function handleThings(opts = {}) {
// ...
}
21.7 Map結構
注意區分Object和Map,只有模擬現實世界的實體對象時,才使用Object。若是隻是須要key: value的數據結構,使用Map結構。由於Map有內建的遍
歷機制。
let map = new Map(arr);
for (let key of map.keys()) {
console.log(key);
}
for (let value of map.values()) {
console.log(value);
}
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
21.8 Class
老是用Class,取代須要prototype的操做。由於Class的寫法更簡潔,更易於理解。
// bad
function Queue(contents = []) {
this._queue = [...contents];
}
Queue.prototype.pop = function() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}
// good
class Queue {
constructor(contents = []) {
this._queue = [...contents];
}
pop() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}
}
使用extends實現繼承,由於這樣更簡單,不會有破壞instanceof運算的危險。
// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function() {
return this._queue[0];
}
// good
class PeekableQueue extends Queue {
peek() {
return this._queue[0];
}
}
21.9 模塊
首先,Module語法是JavaScript模塊的標準寫法,堅持使用這種寫法。使用import取代require。
// bad
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;
// good
import { func1, func2 } from 'moduleA';
使用export取代module.exports。
// commonJS的寫法
var React = require('react');
var Breadcrumbs = React.createClass({
render() {
return <nav />;
}
});
module.exports = Breadcrumbs;
// ES6的寫法
import React from 'react';
const Breadcrumbs = React.createClass({
render() {
return <nav />;
}
});
export default Breadcrumbs
若是模塊只有一個輸出值,就使用export default,若是模塊有多個輸出值,就不使用export default,不要export default與普通的export同時使
用。
不要在模塊輸入中使用通配符。由於這樣能夠確保你的模塊之中,有一個默認輸出(export default)。
// bad
import * as myObject './importModule';
// good
import myObject from './importModule';
若是模塊默認輸出一個函數,函數名的首字母應該小寫。
function makeStyleGuide() {
}
export default makeStyleGuide;
若是模塊默認輸出一個對象,對象名的首字母應該大寫。
const StyleGuide = {
es6: {
}
};
export default StyleGuide;
21.10 ESLint的使用
ESLint是一個語法規則和代碼風格的檢查工具,能夠用來保證寫出語法正確、風格統一的代碼。
首先,安裝ESLint。
$ npm i -g eslint
而後,安裝Airbnb語法規則。
$ npm i -g eslint-config-airbnb
最後,在項目的根目錄下新建一個.eslintrc文件,配置ESLint。
{
"extends": "eslint-config-airbnb"
}
如今就能夠檢查,當前項目的代碼是否符合預設的規則。
index.js文件的代碼以下。
var unusued = 'I have no purpose!';
function greet() {
var message = 'Hello, World!';
alert(message);
}
greet();
使用ESLint檢查這個文件。
$ eslint index.js
index.js
1:5 error unusued is defined but never used no-unused-vars
4:5 error Expected indentation of 2 characters but found 4 indent
5:5 error Expected indentation of 2 characters but found 4 indent
✖ 3 problems (3 errors, 0 warnings)
上面代碼說明,原文件有三個錯誤,一個是定義了變量,卻沒有使用,另外兩個是行首縮進爲4個空格,而不是規定的2個空格。
22 讀懂 ECMAScript 規格
22.1 概述
規格文件是計算機語言的官方標準,詳細描述語法規則和實現方法。
通常來講,沒有必要閱讀規格,除非你要寫編譯器。由於規格寫得很是抽象和精煉,又缺少實例,不容易理解,並且對於解決實際的應用問題,幫助
不大。可是,若是你遇到疑難的語法問題,實在找不到答案,這時能夠去查看規格文件,瞭解語言標準是怎麼說的。規格是解決問題的「最後一招」。
這對JavaScript語言頗有必要。由於它的使用場景複雜,語法規則不統一,例外不少,各類運行環境的行爲不一致,致使奇怪的語法問題層出不窮,任
何語法書都不可能囊括全部狀況。查看規格,不失爲一種解決語法問題的最可靠、最權威的終極方法。
本章介紹如何讀懂ECMAScript 6的規格文件。
ECMAScript 6的規格,能夠在ECMA國際標準組織的官方網站(www.ecma-international.org/ecma-262/6.0/)免費下載和在線閱讀。
這個規格文件至關龐大,一共有26章,A4打印的話,足足有545頁。它的特色就是規定得很是細緻,每個語法行爲、每個函數的實現都作了詳盡
的清晰的描述。基本上,編譯器做者只要把每一步翻譯成代碼就能夠了。這很大程度上,保證了全部ES6實現都有一致的行爲。
ECMAScript 6規格的26章之中,第1章到第3章是對文件自己的介紹,與語言關係不大。第4章是對這門語言整體設計的描述,有興趣的讀者能夠讀一
下。第5章到第8章是語言宏觀層面的描述。第5章是規格的名詞解釋和寫法的介紹,第6章介紹數據類型,第7章介紹語言內部用到的抽象操做,第8章
介紹代碼如何運行。第9章到第26章介紹具體的語法。
對於通常用戶來講,除了第4章,其餘章節都涉及某一方面的細節,不用通讀,只要在用到的時候,查閱相關章節便可。下面經過一些例子,介紹如何
使用這份規格。
21.2 相等運算符
相等運算符(==)是一個很讓人頭痛的運算符,它的語法行爲多變,不符合直覺。這個小節就看看規格怎麼規定它的行爲。
請看下面這個表達式,請問它的值是多少。
0 == null
若是你不肯定答案,或者想知道語言內部怎麼處理,就能夠去查看規格,7.2.12小節是對相等運算符(==)的描述。
規格對每一種語法行爲的描述,都分紅兩部分:先是整體的行爲描述,而後是實現的算法細節。相等運算符的整體描述,只有一句話。
「The comparison x == y, where x and y are values, produces true or false.」
上面這句話的意思是,相等運算符用於比較兩個值,返回true或false。
下面是算法細節。
1. ReturnIfAbrupt(x).
2. ReturnIfAbrupt(y).
3. If Type(x) is the same as Type(y), then
Return the result of performing Strict Equality Comparison x === y.
4. If x is null and y is undefined, return true.
5. If x is undefined and y is null, return true.
6. If Type(x) is Number and Type(y) is String,
return the result of the comparison x == ToNumber(y).
7. If Type(x) is String and Type(y) is Number,
return the result of the comparison ToNumber(x) == y.
8. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
9. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
10. If Type(x) is either String, Number, or Symbol and Type(y) is Object, then
return the result of the comparison x == ToPrimitive(y).
11. If Type(x) is Object and Type(y) is either String, Number, or Symbol, then
return the result of the comparison ToPrimitive(x) == y.
12. Return false.
上面這段算法,一共有12步,翻譯以下。
1. 若是x不是正常值(好比拋出一個錯誤),中斷執行。
2. 若是y不是正常值,中斷執行。
3. 若是Type(x)與Type(y)相同,執行嚴格相等運算x === y。
4. 若是x是null,y是undefined,返回true。
5. 若是x是undefined,y是null,返回true。
6. 若是Type(x)是數值,Type(y)是字符串,返回x == ToNumber(y)的結果。
7. 若是Type(x)是字符串,Type(y)是數值,返回ToNumber(x) == y的結果。
8. 若是Type(x)是布爾值,返回ToNumber(x) == y的結果。
9. 若是Type(y)是布爾值,返回x == ToNumber(y)的結果。
10. 若是Type(x)是字符串或數值或Symbol值,Type(y)是對象,返回x == ToPrimitive(y)的結果。
11. 若是Type(x)是對象,Type(y)是字符串或數值或Symbol值,返回ToPrimitive(x) == y的結果。
12. 返回false。
因爲0的類型是數值,null的類型是Null(這是規格4.3.13小節的規定,是內部Type運算的結果,跟typeof運算符無關)。所以上面的前11步都得不到
結果,要到第12步才能獲得false。
0 == null // false
21.3 數組的空位
下面再看另外一個例子。
const a1 = [undefined, undefined, undefined];
const a2 = [, , ,];
a1.length // 3
a2.length // 3
a1[0] // undefined
a2[0] // undefined
a1[0] === a2[0] // true
上面代碼中,數組a1的成員是三個undefined,數組a2的成員是三個空位。這兩個數組很類似,長度都是3,每一個位置的成員讀取出來都
是undefined。
可是,它們實際上存在重大差別。
0 in a1 // true
0 in a2 // false
a1.hasOwnProperty(0) // true
a2.hasOwnProperty(0) // false
Object.keys(a1) // ["0", "1", "2"]
Object.keys(a2) // []
a1.map(n => 1) // [1, 1, 1]
a2.map(n => 1) // [, , ,]
上面代碼一共列出了四種運算,數組a1和a2的結果都不同。前三種運算(in運算符、數組的hasOwnProperty方法、Object.keys方法)都說明,數
組a2取不到屬性名。最後一種運算(數組的map方法)說明,數組a2沒有發生遍歷。
爲何a1與a2成員的行爲不一致?數組的成員是undefined或空位,到底有什麼不一樣?
規格的12.2.5小節《數組的初始化》給出了答案。
「Array elements may be elided at the beginning, middle or end of the element list. Whenever a comma in the element list is not preceded by
an AssignmentExpression (i.e., a comma at the beginning or after another comma), the missing array element contributes to the length of the
Array and increases the index of subsequent elements. Elided array elements are not defined. If an element is elided at the end of an array,
that element does not contribute to the length of the Array.」
翻譯以下。
"數組成員能夠省略。只要逗號前面沒有任何表達式,數組的length屬性就會加1,而且相應增長其後成員的位置索引。被省略的成員不會被定
義。若是被省略的成員是數組最後一個成員,則不會致使數組length屬性增長。」
上面的規格說得很清楚,數組的空位會反映在length屬性,也就是說空位有本身的位置,可是這個位置的值是未定義,即這個值是不存在的。若是一
定要讀取,結果就是undefined(由於undefined在JavaScript語言中表示不存在)。
這就解釋了爲何in運算符、數組的hasOwnProperty方法、Object.keys方法,都取不到空位的屬性名。由於這個屬性名根本就不存在,規格里面沒說
要爲空位分配屬性名(位置索引),只說要爲下一個元素的位置索引加1。
至於爲何數組的map方法會跳過空位,請看下一節。
21.4 數組的map方法
規格的22.1.3.15小節定義了數組的map方法。該小節先是整體描述map方法的行爲,裏面沒有提到數組空位。
後面的算法描述是這樣的。
1. Let O be ToObject(this value).
2. ReturnIfAbrupt(O).
3. Let len be ToLength(Get(O, "length")).
4. ReturnIfAbrupt(len).
5. If IsCallable(callbackfn) is false, throw a TypeError exception.
6. If thisArg was supplied, let T be thisArg; else let T be undefined.
7. Let A be ArraySpeciesCreate(O, len).
8. ReturnIfAbrupt(A).
9. Let k be 0.
10. Repeat, while k < len
a. Let Pk be ToString(k).
b. Let kPresent be HasProperty(O, Pk).
c. ReturnIfAbrupt(kPresent).
d. If kPresent is true, then
d-1. Let kValue be Get(O, Pk).
d-2. ReturnIfAbrupt(kValue).
d-3. Let mappedValue be Call(callbackfn, T, «kValue, k, O»).
d-4. ReturnIfAbrupt(mappedValue).
d-5. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue).
d-6. ReturnIfAbrupt(status).
e. Increase k by 1.
11. Return A.
翻譯以下。
1. 獲得當前數組的this對象
2. 若是報錯就返回
3. 求出當前數組的length屬性
4. 若是報錯就返回
5. 若是map方法的參數callbackfn不可執行,就報錯
6. 若是map方法的參數之中,指定了this,就讓T等於該參數,不然T爲undefined
7. 生成一個新的數組A,跟當前數組的length屬性保持一致
8. 若是報錯就返回
9. 設定k等於0
10. 只要k小於當前數組的length屬性,就重複下面步驟
a. 設定Pk等於ToString(k),即將K轉爲字符串
b. 設定kPresent等於HasProperty(O, Pk),即求當前數組有沒有指定屬性
c. 若是報錯就返回
d. 若是kPresent等於true,則進行下面步驟
d-1. 設定kValue等於Get(O, Pk),取出當前數組的指定屬性
d-2. 若是報錯就返回
d-3. 設定mappedValue等於Call(callbackfn, T, «kValue, k, O»),即執行回調函數
d-4. 若是報錯就返回
d-5. 設定status等於CreateDataPropertyOrThrow (A, Pk, mappedValue),即將回調函數的值放入A數組的指定位置
d-6. 若是報錯就返回
e. k增長1
11. 返回A
仔細查看上面的算法,能夠發現,當處理一個全是空位的數組時,前面步驟都沒有問題。進入第10步的b時,kpresent會報錯,由於空位對應的屬性
名,對於數組來講是不存在的,所以就會返回,不會進行後面的步驟。
const arr = [, , ,];
arr.map(n => {
console.log(n);
return 1;
}) // [, , ,]
上面代碼中,arr是一個全是空位的數組,map方法遍歷成員時,發現是空位,就直接跳過,不會進入回調函數。所以,回調函數裏面的console.log語
句根本不會執行,整個map方法返回一個全是空位的新數組。
V8引擎對map方法的實現以下,能夠看到跟規格的算法描述徹底一致。
function ArrayMap(f, receiver) {
CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");
// Pull out the length so that modifications to the length in the
// loop will not affect the looping and side effects are visible.
var array = TO_OBJECT(this);
var length = TO_LENGTH_OR_UINT32(array.length);
return InnerArrayMap(f, receiver, array, length);
}
function InnerArrayMap(f, receiver, array, length) {
if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);
var accumulator = new InternalArray(length);
var is_array = IS_ARRAY(array);
var stepping = DEBUG_IS_STEPPING(f);
for (var i = 0; i < length; i++) {
if (HAS_INDEX(array, i, is_array)) {
var element = array[i];
// Prepare break slots for debugger step in.
if (stepping) %DebugPrepareStepInIfStepping(f);
accumulator[i] = %_Call(f, receiver, element, i, array);
}
}
var result = new GlobalArray();
%MoveArrayContents(accumulator, result);
r
e
t
u
r
n
r
e
s
u
l
t
;
}
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息