JavaScript 基本語法,JavaScript 引用類型, JavaScript 面向對象程序設計、函數表達式和異步編程 三篇筆記是對《JavaScript 高級程序設計》和 《ECMAScript 6入門》 兩本書的總結整理。javascript
一個完整的JavaScript實現應該由三個不一樣的部分組成:核心(ECMAScript)、文檔對象模型(DOM)、瀏覽器對象模型(BOM)php
JavaScript實現了ECMAScript,Adobe ActionScript一樣也實現了ECMAScript。前端
使用 <script>
元素的方式有兩種:直接在頁面中嵌入 JavaScript 代碼和包含外部 JavaScript文件。 在使用 <script>
元素嵌入 JavaScript代碼時,只須爲 <script>
指定 type 屬性。java
包含在 <script>
元素內部的JavaScript代碼將被從上至下依次解釋。就拿前面這個例子來講,解釋器會解釋一個函數的定義,而後將該定義保存在本身的環境當中。在解釋器對 <script>
元素內部的全部代碼求值完畢之前,頁面中的其他內容都不會被瀏覽器加載或顯示。 在使用 <script>
嵌入JavaScript代碼時,記住不要在代碼中的任何地方出現 </script>
字符串。例如,瀏覽器在加載下面所示的代碼時就會產生一個錯誤: <script type="text/javascript"> function sayScript(){ alert("</script>"); } </script>
由於按照解析嵌入式代碼的規則,當瀏覽器遇到字符串 </script>
時,就會認爲那是結束的 </script>
node
若是是在 XHTML文檔中,也能夠省略前面示例代碼中結束的 </script>
標籤,例如: <script type="text/javascript" src="example.js" />
可是,不能在 HTML文檔使用這種語法。緣由是這種語法不符合 HTML規範python
爲了不延遲瀏覽器出現空白,現代Web應用程序通常都把所有JavaScript引用放在 <body>
元素中頁面內容的後面,jquery
按照慣例,外部 JavaScript文件帶有.js擴展名。但這個擴展名不是必需的,由於瀏覽器不會檢查包含 JavaScript的文件的擴展名。這樣一來,使用 JSP、PHP或其餘服務器端語言動態生成 JavaScript代碼也就成爲了可能。可是,服務器一般仍是須要看擴展名決定爲響應應用哪一種 MIME 類型。若是不使用.js 擴展名,請確保服務器能返回正確的MIME類型。 須要注意的是,帶有 src 屬性的 <script>
元素不該該在其 <script>
和 </script>
標籤之間再包含額外的 JavaScript代碼。若是包含了嵌入的代碼,則只會下載並執行外部腳本文件,嵌入的代碼會被忽略。nginx
不管如何包含代碼,只要不存在 defer 和 async 屬性,瀏覽器都會按照 <script>
元素在頁面中出現的前後順序對它們依次進行解析git
HTML 4.01爲 <script>
標籤訂義了defer屬性。這個屬性的用途是代表腳本在執行時不會影響頁面的構造。也就是說,腳本會被延遲到整個頁面都解析完畢後再運行。所以,在 <script>
元素中設置deferes6
在現實當中,延遲腳本並不必定會按照順序執行,也不必定會在 DOMContentLoaded 事件觸發前執行,所以最好只包含一個延遲腳本。 前面提到過,defer 屬性只適用於外部腳本文件。這一點在 HTML5 中已經明確規定,所以支持HTML5的實現會忽略給嵌入腳本設置的 defer 屬性
指定async屬性的目的是不讓頁面等待兩個腳本下載和執行,從而異步加載頁面其餘內容。爲此,建議異步腳本不要在加載期間修改DOM。 異步腳本必定會在頁面的 load 事件前執行,但可能會在 DOMContentLoaded 事件觸發以前或以後執行。
一樣與defer相似,async只適用於外部腳本文件,並告訴瀏覽器當即下載文件。但與defer不一樣的是,標記爲async的腳本並不保證按照指定它們的前後順序執行。
保證讓相同代碼在 XHTML中正常運行的第二個方法,就是用一個 CData片斷來包含 JavaScript代碼。在XHTML(XML)中,CData片斷是文檔中的一個特殊區域,這個區域中能夠包含不須要解析的任意格式的文本內容。
但因爲全部瀏覽器都已經支持 JavaScript,所以也就沒有必要再使用下面這種格式了。
//<!--
代碼
//-->
通常認爲最好的作法仍是儘量使用外部文件來包含 JavaScript代碼。可維護性,可緩存,適應將來
文檔模式是:混雜模式(quirks mode)和標準模式(standards mode)。混雜模式會讓IE的行爲與(包含非標準特性的)IE5相同,而標準模式則讓IE的行爲更接近標準行爲。雖然這兩種模式主要影響CSS內容的呈現,但在某些狀況下也會影響到 JavaScript的解釋執行。
對於準標準模式,則能夠經過使用過渡型(transitional)或框架集型(frameset)文檔類型來觸發,
若是在文檔開始處沒有發現文檔類型聲明,則全部瀏覽器都會默認開啓混雜模式。但採用混雜模式不是什麼值得推薦的作法,由於不一樣瀏覽器在這種模式下的行爲差別很是大,若是不使用某些 hack 技術,跨瀏覽器的行爲根本就沒有一致性可言。
包含在 <noscript>
元素中的內容只有在這些狀況下才會顯示出來:瀏覽器不支持腳本;瀏覽器支持腳本,但腳本被禁用。
在 Chrome 地址欄中輸入 chrome://flags/#enable-javascript-harmony,啓用實驗性 JavaScript
各個平臺對ECMAScript 6的支持狀況能夠查看 https://kangax.github.io/compat-table/es6/
鑑於如今瀏覽器並無徹底支持ECMASctipt6,因此能夠用ES6的方式編寫代碼,以後用 Babel 或谷歌的 Traceur 進行轉碼
另外node中使用能夠加參數 --harmony
ECMAScript中的一切(變量、函數名和操做符)都區分大小寫
標識符能夠是按照下列格式規則組合起來的一或多個字符:第一個字符必須是一個字母、下劃線( _
)或一個美圓符號( $
);其餘字符能夠是字母、下劃線、美圓符號或數字。 標識符中的字母也能夠包含擴展的ASCII或Unicode字母字符(如À和Æ),但咱們不推薦這樣作。 按照慣例,ECMAScript標識符采用駝峯大小寫格式,也就是第一個字母小寫,剩下的每一個單詞的首字母大寫
C風格的註釋,包括單行註釋和塊級註釋
ECMAScript中的語句以一個分號結尾;若是省略分號,則由解析器肯定語句的結尾。最好不省略分號,省略分號,解釋器會猜想在什麼位置加分號,這樣可能會照成與預期不一樣的結果。如
return //瀏覽器在這裏加入分號,而後1+2就不會被返回了
1 + 2
要在整個腳本中啓用嚴格模式,能夠在頂部添加以下代碼: 「use strict」;
ECMAScript 的變量是鬆散類型的,所謂鬆散類型就是能夠用來保存任何類型的數據。定義變量應該使用var操做符(不使用var操做符將會定義一個全局變量,這種方式不被推薦),後面跟一個變量名。 var message
像這樣只是聲明一個變量,並無初始化,它的值將是undefined。
變量聲明具備hoisting機制,JavaScript引擎在執行的時候,會把全部變量的聲明都提高到當前做用域的最前面。
var v = "hello";
(function(){
console.log(v);
var v = "world";
var f = function(){};
})();
執行結果是undefined。這就是由於變量提高,上面的代碼實際上會是這樣的
var v;
v = "hello";
(function(){
//覆蓋全局的v變量,而且沒有初始化因此是undefined
var v,f;
console.log(v);
v = "world";
//函數表達式不會被提高,可是函數聲明會,後面還會講
f = function(){};
})();
嚴格模式下,不能定義名爲eval或arguments的變量,不然會致使語法錯誤。
ES6中還能夠使用let生命變量不一樣的是let聲明的變量只在其所在代碼塊內有效(意味着ES6支持塊級做用域了),而且不會發生「變量提高「現象(注意引號,當進入包含let的做用域,let所聲明的變量以建立可是不能夠使用,讀寫都會拋錯,直到聲明語句)。let不容許在相同做用域內,重複聲明同一個變量。
(function(){
var v = "world";
if (true) {
v = 'hello';
let v;
}
})();
只要塊級做用域內存在let命令,它所聲明的變量就「綁定」(binding)這個區域,再也不受外部的影響。上面的代碼中if塊內v經過let聲明,再也不受外部變量v影響,可是因爲在let聲明前對v賦值,因此會報錯。
總之,在代碼塊內,使用let命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱TDZ)。
下面兩個函數中使用let重複聲明變量都會報錯
function () {
let a = 10;
var a = 1;
}
function () {
let a = 10;
let a = 1;
}
ECMAScript6中const也用來聲明變量,可是聲明的是常量。一旦聲明,常量的值就不能改變。const的做用域與let命令相同:只在聲明所在的塊級做用域內有效;不存在「變量提高「現象,只能在聲明的位置後面使用;也不可重複聲明。和Java中final有點相似,不可變是指其指向的對象不可變,可是對象內部屬性能夠變。
const foo = {};
foo.prop = 123;
foo.prop // 123
foo = {} // 不起做用
若是真的想將對象凍結,應該使用Object.freeze方法。
const foo = Object.freeze({});
foo.prop = 123; // 不起做用
除了將對象自己凍結,對象的屬性也應該凍結。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, value) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
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
ES6規定,var和function聲明的全局變量,屬於全局對象的屬性;let命令、const命令、class命令聲明的全局變量,不屬於全局對象的屬性。
var a = 1;
//這裏this即全局變量,在瀏覽器中就是window,node中是global
this.a;
let b = 1;
this.b; //undefined
模塊中運行的全局變量,都是當前模塊的屬性,而不是node頂層對象的屬性。
ECMAScript中有5種簡單數據類型(也稱爲基本數據類型):Undefined、Null、Boolean、Number和String。還有1種複雜數據類型——Object,其本質是一組 無序 的名值對組成
ES6引入了一種新的基本數據類型Symbol,是一種特殊的、不可變的數據類型,能夠做爲對象屬性的標識符使用。
如今共有七種數據類型:Undefined, Null, Boolean, String, Symbol, Number和Object.
typeof的返回值
typeof操做符的操做數能夠是變量(message),也能夠是數值字面量。注意,typeof是一個操做符而不是函數,所以例子中的圓括號儘管能夠使用,但不是必需的。
Undefined類型只有一個值,即特殊的undefined。在使用var聲明變量但未對其加以初始化時,這個變量的值就是undefined
對未初始化和未聲明的變量執行 typeof 操做符都返回 undefined 值,可是若是直接訪問未聲明的變量就會報錯了。
對於還沒有聲明過的變量,只能執行一項操做,即便用typeof操做符檢測其數據類型,這也是typeof存在的最大意義了。
Null類型是第二個只有一個值的數據類型,這個特殊的值是 null。從邏輯角度來看,null 值表示一個空對象指針,而這也正是使用 typeof 操做符檢測 null 值時會返回」object」的緣由
實際上,undefined值是派生自null值的, null == undefined
將會返回true。
若是定義的變量準備在未來用於保存對象,那麼最好將該變量初始化爲null而不是其餘值。這樣一來,只要直接檢查null 值就能夠知道相應的變量是否已經保存了一個對象的引用
能夠對任何數據類型的值調用Boolean()函數,並且總會返回一個Boolean值,你也能夠在任何數據前加 !!
使其轉化爲Boolean類型。
轉換規則
數據類型 | 轉換爲true的值 | 轉換爲false的值 |
---|---|---|
Boolean | true | false |
String | 非空字符串 | 「」 |
Number | 非零數字值(包括無窮大) | 0和NaN |
Object | 任何對象 | null |
Undefined | n/a(不適用) | undefined |
八進制字面量在嚴格模式下是無效的
默認狀況下,ECMAScript會將那些小數點後面帶有 6個零以上的浮點數值轉換爲以 e表示法表示的數值(例如,0.0000003會被轉換成3e-7)
若是浮點數值自己表示的就是一個整數(如1.0),那麼該值也會被轉換爲整數
永遠不要測試某個特定的浮點數值。 關於浮點數值計算會產生舍入偏差的問題,有一點須要明確:這是使用基於IEEE754數值的浮點計算的通病,ECMAScript並不是獨此一家;其餘使用相同數值格式的語言也存在這個問題。
所謂浮點數值,就是該數值中必須包含一個小數點,而且小數點後面必須至少有一位數字。雖然小數點前面能夠沒有整數,但咱們不推薦這種寫法
isNaN()函數。這個函數接受一個參數,該參數能夠是任何類型,而函數會幫咱們肯定這個參數是否「不是數值」
ECMAScript可以表示的最小數值保存在 Number.MIN_VALUE 中——在大多數瀏覽器中,這個值是 5e-324;可以表示的最大數值保存在Number.MAX_VALUE中——在大多數瀏覽器中,這個值是1.7976931348623157e+308。若是某次計算的結果獲得了一個超出JavaScript數值範圍的值,那麼這個數值將被自動轉換成特殊的 Infinity 值
要想肯定一個數值是否是有窮的(換句話說,是否是位於最小和最大的數值之間),能夠使用 isFinite()函數
NaN,即非數值(Not a Number)是一個特殊的數值,這個數值用於表示一個原本要返回數值的操做數未返回數值的狀況(這樣就不會拋出錯誤了)。例如,在其餘編程語言中,任何數值除以0都會致使錯誤,從而中止代碼執行。但在ECMAScript中,任何數值除以0會返回NaN(實際上只有0除以0纔會返回NaN,正數除以0返回Infinity,負數除以0返回-Infinity),所以不會影響其餘代碼的執行。 NaN自己有兩個非同尋常的特色。首先,任何涉及 NaN 的操做(例如 NaN/10)都會返回 NaN,這個特色在多步計算中有可能致使問題。其次,NaN與任何值都不相等,包括NaN自己
有3個函數能夠把非數值轉換爲數值:Number()、parseInt()和parseFloat()(能夠在字符串前加+
將其轉爲數字,如 +'10'
)。第一個函數,即轉型函數 Number()能夠用於任何數據類型,而另兩個函數則專門用於把字符串轉換成數值
parseInt這個函數提供第二個參數:轉換時使用的基數(即多少進制)。
parseInt()函數在轉換字符串時,更多的是看其是否符合數值模式。它會忽略字符串前面的空格,直至找到第一個非空格字符。若是第一個字符不是數字字符或者負號,parseInt()就會返回 NaN;也就是說,用 parseInt()轉換空字符串會返回 NaN(Number()對空字符返回0)。若是第一個字符是數字字符,parseInt()會繼續解析第二個字符,直到解析完全部後續字符或者遇到了一個非數字字符。例如,」1234blue」會被轉換爲1234,由於」blue」會被徹底忽略。相似地,」22.5」會被轉換爲22,由於小數點並非有效的數字字符。
除了第一個小數點有效以外,parseFloat()與 parseInt()的第二個區別在於它始終都會忽略前導的零。parseFloat()能夠識別前面討論過的全部浮點數值格式,也包括十進制整數格式。但十六進制格式的字符串則始終會被轉換成0。因爲parseFloat()只解析十進制值,所以它沒有用第二個參數指定基數的用法。最後還要注意一點:若是字符串包含的是一個可解析爲整數的數(沒有小數點,或者小數點後都是零),parseFloat()會返回整數。
與PHP中的雙引號和單引號會影響對字符串的解釋方式不一樣,ECMAScript中的這兩種語法形式沒有什麼區別。
轉義字符被做爲一個字符來解析
數值、布爾值、對象和字符串值(沒錯,每一個字符串也都有一個toString()方法,該方法返回字符串的一個副本)都有toString()方法。但null和undefined值沒有這個方法。 多數狀況下,調用toString()方法沒必要傳遞參數。可是,在調用數值的toString()方法時,能夠傳遞一個參數:輸出數值的基數
不知道要轉換的值是否是null或undefined的狀況下,還能夠使用轉型函數String(),這個函數可以將任何類型的值轉換爲字符串。
能夠使用加號 +
操做符把某個值與以空字符加在一塊兒來轉換爲字符串。
要把某個值轉換爲字符串,能夠使用加號操做符(3.5 節討論)把它與一個字符串(」」)加在一塊兒。
Symbol,表示獨一無二的值。對象的屬性名如今能夠有兩種類型,一種是原來就有的字符串,另外一種就是新增的Symbol類型。凡是屬性名屬於Symbol類型,就都是獨一無二的,能夠保證不會與其餘屬性名產生衝突。
注意,Symbol函數前不能使用new命令,不然會報錯。這是由於生成的Symbol是一個原始類型的值,不是對象。也就是說,因爲Symbol值不是對象,因此不能添加屬性。基本上,它是一種相似於字符串的數據類型。
Symbol函數能夠接受一個字符串做爲參數,表示對Symbol實例的描述,主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。
注意,Symbol函數的參數只是表示對當前Symbol值的描述,所以相同參數的Symbol函數的返回值是不相等的。
Symbol值不能與其餘類型的值進行運算,會報錯。可是,Symbol值能夠轉爲字符串。
var sym = Symbol('My symbol');
var a = "your symbol is " + sym; //報錯
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My 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!"
注意,Symbol值做爲對象屬性名時,不能用點運算符。同理,在對象的內部,使用Symbol值定義屬性時,Symbol值必須放在方括號之中。
let obj = {
[s](arg) { ... }
};
Symbol類型還能夠用於定義一組常量,保證這組常量的值都是不相等的。
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn'),
};
log(log.levels.DEBUG, 'debug message');
log(log.levels.INFO, 'info message');
還有一點須要注意,Symbol值做爲屬性名時,該屬性仍是公開屬性,不是私有屬性。
屬性名遍歷
Symbol做爲屬性名,該屬性不會出如今for…in、for…of循環中,也不會被Object.keys()、Object.getOwnPropertyNames()返回。可是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,能夠獲取指定對象的全部Symbol屬性名。
Object.getOwnPropertySymbols方法返回一個數組,成員是當前對象的全部用做屬性名的Symbol值。
Symbol.for方法接受一個字符串做爲參數,而後搜索有沒有以該參數做爲名稱的Symbol值。若是有,就返回這個Symbol值,不然就新建並返回一個以該字符串爲名稱的Symbol值。
var a = Symbol('foo');
var b = Symbol('foo');
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
a === s2 // false
a === b // false
Symbol.for()
與 Symbol()
這兩種寫法,都會生成新的Symbol。它們的區別是,前者會被登記在全局環境中供搜索,後者不會。 Symbol.for()
不會每次調用就返回一個新的Symbol類型的值,而是會先檢查給定的key是否已經存在,若是不存在纔會新建一個值。因爲Symbol()寫法沒有登記機制
Symbol.keyFor方法返回一個已登記的Symbol類型值的key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // 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
除了定義本身使用的Symbol值之外,ES6還提供一些內置的Symbol值,指向語言內部使用的方法。
對象的Symbol.hasInstance屬性,指向一個內部方法。該對象使用instanceof運算符時,會調用這個方法,判斷該對象是否爲某個構造函數的實例。好比, foo instanceof Foo
在語言內部,實際調用的是 Foo[Symbol.hasInstance](foo)
。
對象的Symbol.isConcatSpreadable屬性,指向一個方法。該對象使用Array.prototype.concat()時,會調用這個方法,返回一個布爾值,表示該對象是否能夠擴展成數組。
class A1 extends Array {
[Symbol.isConcatSpreadable]() {
return true;
}
}
class A2 extends Array {
[Symbol.isConcatSpreadable]() {
return 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]]
對象的Symbol.isRegExp屬性,指向一個方法。該對象被用做正則表達式時,會調用這個方法,返回一個布爾值,表示該對象是否爲一個正則對象。
對象的Symbol.match屬性,指向一個函數。當執行 str.match(myObject)
時,若是該屬性存在,會調用它,返回該方法的返回值。
對象的Symbol.replace屬性,指向一個方法,當該對象被String.prototype.replace方法調用時,會返回該方法的返回值。
對象的Symbol.search屬性,指向一個方法,當該對象被String.prototype.search方法調用時,會返回該方法的返回值。
對象的Symbol.split屬性,指向一個方法,當該對象被String.prototype.split方法調用時,會返回該方法的返回值。
對象的Symbol.iterator屬性,指向一個方法,即該對象進行for…of循環時,會調用這個方法,返回該對象的Iterator對象。
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
對象的Symbol.toPrimitive屬性,指向一個方法。該對象被轉爲原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。
對象的Symbol.toStringTag屬性,指向一個方法。在該對象上面調用 Object.prototype.toString
方法時,若是這個屬性存在,它的返回值會出如今toString方法返回的字符串之中,表示對象的類型。也就是說,這個屬性能夠用來定製 [object Object]
或 [object Array]
中object後面的那個字符串。
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
對象的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
}
var o = new Object();
在ECMAScript中,若是不給構造函數傳遞參數,則能夠省略後面的那一對圓括號。
Object每一個實例都具備下列屬性和方法
在ECMAScript中,(就像 Java 中的 java.lang.Object 對象同樣)Object 類型是全部它的實例的基礎
++,--,+,-
)ECMAScript 操做符的不同凡響之處在於,它們可以適用於不少值,例如字符串、數字值、布爾值,甚至對象。不過,在應用於對象時,相應的操做符一般都會調用對象的valueOf()和(或)toString()方法,以便取得能夠操做的值。
應用於非數值的值時,遞增和遞減操做符執行前,該值會被轉換爲數值,而後在執行遞增遞減。對象是先調用它們的valueOf()和(或)toString()方法,再轉換獲得的值。
在對非數值應用一元加操做符時,該操做符會像Number()轉型函數同樣對這個值執行轉換。如+'10' === 10 //true
~,&,|,^,<<,>>,>>>
)負數一樣以二進制碼存儲,但使用的格式是二進制補碼
ECMAScript中的全部數值都以IEEE-754 64位格式存儲,但位操做符並不直接操做64位的值。而是先將64位的值轉換成32位的整數,而後執行操做,最後再將結果轉換回64位。對於開發人員來講,因爲64位存儲格式是透明的,所以整個過程就像是隻存在32位的整數同樣。但這個轉換過程也致使了一個嚴重的副效應,即在對特殊的NaN和Infinity值應用位操做時,這兩個值都會被當成0來處理
默認狀況下,ECMAScript 中的全部整數都是有符號整數
計算補碼的過程
(1) 求這個數值絕對值的二進制碼(例如,要求-18的二進制補碼,先求18的二進制碼); (2) 求二進制反碼,即將0替換爲1,將1替換爲0; (3) 獲得的二進制反碼加1
按位非操做的本質:操做數的負值減1
按位異或操做符由一個插入符號(^)表示
左移操做會以0來填充空位
有符號右移在移位過程當中,原數值中也會出現空位。只不過空位出如今原數值的左側、符號位的右側。而此時ECMAScript會用符號位的值來填充全部空位
首先,無符號右移操做符由3個大於號(>>>)表示,無符號右移是以0來填充空位,其次,無符號右移操做符會把負數的二進制碼當成正數的二進制碼
!,&&,||
)邏輯非操做符首先會將操做數轉換爲布爾值而後再計算。參考前面的Boolean轉換表
同時使用兩個邏輯非操做符,實際上就會模擬 Boolean()轉型函數的行爲。如 !!'' //false
邏輯與和邏輯或在有一個操做符不是布爾值狀況下不必定返回布爾值,遵循下列規則:
邏輯或遵循規則
*,/,%
)在操做數爲非數值的狀況下會執行自動的類型轉換。若是參與乘性計算的某個操做數不是數值,後臺會先使用Number()轉型函數將其轉換爲數值。也就是說,空字符串將被看成0,布爾值true將被看成1。
乘法
除法
求模
+,-
)加法
不過,若是有一個操做數是字符串,那麼就要應用以下規則:
減法
>,<,>=,<=
)對於字符串實際比較的是兩個字符串中對應位置的每一個字符的字符編碼值
按照常理,若是一個值不小於另外一個值,則必定大於或等於那個值,然而,在與NaN進行比較時,下面兩個比較操做的結果都返回了false。
var res1 = NaN < 3 //false
var res2 = NaN >= 3 //false
===,!==,==,!=
)相等和不相等——先轉換再比較,全等和不全等——僅比較而不轉換。
在轉換不一樣的數據類型時,相等和不相等操做符遵循下列基本規則:
因爲相等和不相等操做符存在類型轉換問題,而爲了保持代碼中數據類型的完整性,推薦使用全等和不全等操做符。
boolean_expression?true_value:false_value
)和Java中同樣
,
)在用於賦值時,逗號操做符總會返回表達式中的最後一項
var num = (5,1,3,8,0) //num值爲0
=以及*=、+=等複合賦值運算符
)賦值與複合賦值和其餘語言無太大區別。
ECMAScript6容許按照必定模式,從數組和對象中提取值,對變量進行賦值,這被稱爲解構(Destructuring)。下面是數組解構賦值的例子
var [a, b, c] = [1, 2, 3]; // a即爲1,b爲2,c爲3
let [,,third] = ["foo", "bar", "baz"];//third爲"baz"
let [head, ...tail] = [1, 2, 3, 4]; //head爲1,tail爲[2,3,4],...操做符後面再說
var [foo, [[bar], baz]] = [1, [[2], 3]];
上面代碼表示,能夠從數組中提取值,按照對應位置,對變量賦值。
若是解構不成功,變量的值就等於undefined。如下幾種狀況都屬於解構不成功,foo的值都會等於undefined(下面的代碼在一些環境下會拋異常)。這是由於原始類型的值,會自動轉爲對象,好比數值1轉爲new Number(1),從而致使foo取到undefined。
var [foo] = [];
var [foo] = 1;
var [foo] = false;
var [foo] = NaN;
var [bar, foo] = [1];
另外一種狀況是不徹底解構,即等號左邊的模式,只匹配一部分的等號右邊的數組。這種狀況下,解構依然能夠成功。
let [x, y] = [1, 2, 3]; //x = 1, y = 2
let [a, [b], d] = [1, [2, 3], 4];// a = 1, b = 2, d = 4
若是對undefined或null進行解構,會報錯。
// 報錯
let [foo] = undefined;
let [foo] = null;
這是由於解構只能用於數組或對象。其餘原始類型的值均可以轉爲相應的對象,可是,undefined和null不能轉爲對象,所以報錯。
解構賦值容許指定默認值。
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。
解構賦值不只適用於var命令,也適用於let和const命令。對於Set結構(ECMAScript6新增),也能夠使用數組的解構賦值。事實上,只要某種數據結構具備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接口。解構賦值會依次從這個接口獲取值。
解構不只能夠用於數組,還能夠用於對象。對象的解構與數組有一個重要的不一樣。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值。
var { bar, foo } = { foo: "aaa", bar: "bbb" };//bar = "bbb", foo = "aaa"
var { baz } = { foo: "aaa", bar: "bbb" }; //baz = undefined
var obj = {
p: [
"Hello",
{ y: "World" }
]
};
var { p: [x, { y }] } = obj; //x = "Hello", y = "World"
若是左邊變量名和右邊屬性名不一致
var { foo: baz } = { foo: "aaa", bar: "bbb" }; //baz = "aaa"
默認值生效的條件是,對象的屬性值嚴格等於undefined。
var {x = 3} = {x: undefined}; //x = 3
var {x = 3} = {x: null}; //x = null
若是要將一個已經聲明的變量用於解構賦值,必須很是當心。
// 錯誤的寫法
var x;
{x} = {x:1};
// SyntaxError: syntax error
上面代碼的寫法會報錯,由於JavaScript引擎會將{x}理解成一個代碼塊,從而發生語法錯誤。只有不將大括號寫在行首,避免JavaScript將其解釋爲代碼塊,才能解決這個問題。
// 正確的寫法
({x} = {x:1});
對象的解構賦值,能夠很方便地將現有對象的方法,賦值到某個變量。
let { log, sin, cos } = Math;
上面代碼將Math對象的對數、正弦、餘弦三個方法,賦值到對應的變量上,使用起來就會方便不少。
字符串也能夠解構賦值。這是由於此時,字符串被轉換成了一個相似數組的對象。相似數組的對象都有一個length屬性,所以還能夠對這個屬性解構賦值。
const [a, b, c, d, e] = 'hello';//a = "h", b = "e", c = "l", d = "l", e = "o"
let {length : len} = 'hello'; //len = 5
函數的參數也能夠使用解構。
function add([x, y]){
return x + y;
}
add([1, 2]) // 3
函數參數的解構也能夠使用默認值。
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]
注意,指定函數參數的默認值時,不能採用下面的寫法。
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指定默認值,因此會獲得與前一種寫法不一樣的結果。
變量的解構賦值用途不少。
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({x:1, y:2, z:3})
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");
if, do-while,while,for,label,break,continue,switch和Java沒有太大差異。
推崇始終使用代碼塊,即便要執行的只有一行代碼
像 do-while 這種後測試循環語句最經常使用於循環體中的代碼至少要被執行一次的情形。
加標籤的語句通常都要與for語句等循環語句配合使用。
break和 continue 語句均可以與 label 語句聯合使用,從而返回代碼中特定的位置。這種聯合使用的狀況多發生在循環嵌套的狀況下
建議若是使用label語句,必定要使用描述性的標籤,同時不要嵌套過多的循環
switch語句在比較值時使用的是全等操做符,所以不會發生類型轉換(例如,字符串」10」不等於數值10)。
首先,能夠在switch語句中使用任何數據類型(在不少其餘語言中只能使用數值),不管是字符串,仍是對象都沒有問題。其次,每一個case的值不必定是常量,能夠是變量,甚至是表達式。
因爲 ECMAScript中不存在塊級做用域(ES6已有),所以在循環內部定義的變量也能夠在外部訪問到
ECMAScript對象的屬性沒有順序。所以,經過 for-in 循環輸出的屬性名的順序是不可預測的
for-in語句是一種精準的迭代語句,能夠用來枚舉對象的屬性。
建議在使用for-in循環以前,先檢測確認該對象的值不是null或undefined。
for(var propName in window) {
document.write(propName);
}
因爲大量使用with語句會致使性能降低,同時也會給調試代碼形成困難,所以在開發大型應用程序時,不建議使用with語句。
定義with語句的目的主要是爲了簡化屢次編寫同一個對象的工做,以下面的例子所示:
var qs = location.search.substring(1);
var hostName = location.hostname;
var url = location.href;
上面幾行代碼都包含location對象。若是使用with 語句,能夠把上面的代碼改寫成以下所示:
with(location){
var qs = search.substring(1);
var hostName = hostname;
var url = href;
}
這個重寫後的例子中,使用with 語句關聯了location 對象。這意味着在with 語句的代碼塊內部,每一個變量首先被認爲是一個局部變量,而若是在局部環境中找不到該變量的定義,就會查詢location對象中是否有同名的屬性。若是發現了同名屬性,則以location對象屬性的值做爲變量的值。 嚴格模式下不容許使用with語句,不然將視爲語法錯誤
ES6借鑑C++、Java、C#和Python語言,引入了for…of循環,做爲遍歷全部數據結構的統一的方法。一個數據結構只要部署了 Symbol.iterator
方法,就被視爲具備Iterable接口,就能夠用for…of循環遍歷它的成員。也就是說,for…of循環內部調用的是數據結構的 Symbol.iterator
方法。
for…of循環能夠使用的範圍包括數組、Set和Map結構及其entries,values,keys方法返回的對象、某些相似數組的對象(好比arguments對象、DOM NodeList對象)、後文的Generator對象,以及字符串。
數組原生具有Iterable接口,for…of循環本質上就是調用 Symbol.iterator
產生的Iterator對象,能夠用下面的代碼證實。
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
}
JavaScript原有的for…in循環,只能得到對象的鍵名,不能直接獲取鍵值。ES6提供for…of循環,容許遍歷得到鍵值。
var arr = ["a", "b", "c", "d"];
for (a in arr) {
console.log(a); // 0 1 2 3
}
for (a of arr) {
console.log(a); // a b c d
}
Set和Map結構也原生具備Iterator接口,能夠直接使用for…of循環。
var engines = 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成員的鍵名和鍵值。
並非全部相似數組的對象都具備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);
}
經過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函數將對象從新包裝一下。
以數組爲例,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);
}
嚴格模式對函數有一些限制:
return語句也能夠不帶有任何返回值。在這種狀況下,函數在中止執行後將返回undefined值。
即使你定義的函數只接收兩個參數,在調用這個函數時也未必必定要傳遞兩個參數。能夠傳遞一個、三個甚至不傳遞參數,而解析器永遠不會有什麼怨言。之因此會這樣,緣由是ECMAScript中的參數在內部是用一個數組來表示的。函數接收到的始終都是這個數組,而不關心數組中包含哪些參數(若是有參數的話)。若是這個數組中不包含任何元素,無所謂;若是包含多個元素,也沒有問題。實際上,在函數體內能夠經過arguments對象來訪問這個參數數組,從而獲取傳遞給函數的每個參數。
其實,arguments對象只是與數組相似(它並非Array的實例),由於能夠使用方括號語法訪問它的每個元素(即第一個元素是arguments[0],第二個元素是arguments[1],以此類推),使用length屬性來肯定傳遞進來多少個參數。
function doAdd(num1, num2) {
arguments[1] = 10;
alert(arguments[0] + num2);
}
修改arguments[1],也就修改了num2,結果它們的值都會變成10。不過,這並非說讀取這兩個值會訪問相同的內存空間;它們的內存空間是獨立的,但它們的值會同步。但這種影響是單向的,修改命名參數不會改變arguments中對應的值。另外還要記住,若是隻傳入了一個參數,那麼爲arguments[1]設置的值不會反應到命名參數中。這是由於arguments對象的長度是由傳入的參數個數決定的,不是由定義函數時的命名參數的個數決定的。 關於參數還要記住最後一點:沒有傳遞值的命名參數將自動被賦予undefined值。若是給doAdd只傳一個參數num2就是undefined
嚴格模式對如何使用 arguments 對象作出了一些限制。首先,像前面例子中那樣的賦值會變得無效。也就是說,即便把 arguments[1]設置爲 10,num2 的值仍然仍是 undefined。其次,重寫arguments的值會致使語法錯誤(代碼將不會執行)。 ECMAScript中的全部參數傳遞的都是值,不可能經過引用傳遞參數。
沒有重載
沒有函數簽名,真正的重載是不可能作到的。 若是在ECMAScript中定義了兩個名字相同的函數,則該名字只屬於後定義的函數。
ES6容許使用「箭頭」(=>)定義函數(和Java8中lambda表達式有點相似)
// 基本用法
(param1, param2, paramN) => { statements }
(param1, param2, paramN) => expression // equivalent to: => { return expression; }
// 若是隻有一個參數能夠省略圓括號
singleParam => { statements }
singleParam => expression
//若是沒有參數,則須要一個圓括號
() => { statements }
//若是返回一個對象,必須在對象外面加上括號。
params => ({foo: bar})
// 支持Rest參數
(param1, param2, ...rest) => { statements }
// 支持變量解構
({param1, param2}) => { statements }
箭頭函數的一個用處是簡化回調函數。
// 正常函數寫法
[1,2,3].map(function (x) {
return x * x;
});
// 箭頭函數寫法
[1,2,3].map(x => x * x);
箭頭函數有幾個使用注意點。
上面四點中,第一點尤爲值得注意。this對象的指向是可變的,可是在箭頭函數中,它是固定的。下面的代碼是一個例子,將this對象綁定定義時所在的對象。
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在箭頭函數中被綁定,因此不能用call()、apply()、bind()這些方法去改變this的指向。
箭頭函數內部,還能夠再使用箭頭函數。下面是一個ES5語法的多重嵌套函數。
下面是一個部署管道機制(pipeline)的例子,即前一個函數的輸出是後一個函數的輸入。
//pipeline參數是...funcs,返回值是val => funcs.reduce((a, b) => b(a), val);
//addThenMult參數是val,返回值是 funcs.reduce((a, b) => b(a), val);
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)));
Generator函數是ES6提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。
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對象。
下一步,必須調用Iterator對象的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 }
第三次調用,Generator函數從上次yield語句停下的地方,一直執行到return語句(若是沒有return語句,就執行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句後面的表達式的值(若是沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷已經結束。第四次調用,此時Generator函數已經運行完畢,next方法返回對象的value屬性爲undefined,done屬性爲true。之後再調用next方法,返回的都是這個值。
總結一下,調用Generator函數,返回一個實現了Iterator接口的對象,用來操做內部指針。之後,每次調用Iterator對象的next方法,就會返回一個實現了IteratorResult接口的對象。value屬性表示當前的內部狀態的值,是yield語句後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。
因爲Generator函數返回的Iterator對象,只有調用next方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield語句就是暫停標誌。
Iterator對象next方法的運行邏輯以下。
須要注意的是,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語句。一種修改方法是改用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
任意一個對象的Symbol.iterator屬性,等於該對象的iterator函數,調用該函數會返回該對象的一個Iterator對象。這裏的Iterator對象的Symbol.iterator方法執行後,返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g // true
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:false}
上面代碼中,第二次運行next方法的時候不帶參數,致使y的值等於 2 * undefined
(即NaN),除以3之後仍是NaN,所以返回對象的value屬性也等於NaN。第三次運行Next方法的時候不帶參數,因此z等於undefined,返回對象的value屬性等於 5 + NaN + undefined
,即NaN。
若是向next方法提供參數,返回結果就徹底不同了。
var it = foo(5);
it.next() // { value:6, done:false }
it.next(12) // { value:8, done:false }
it.next(13) // { value:42, done:true }
上面代碼第一次調用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方法開始,參數纔是有效的。
for…of循環能夠自動遍歷Generator函數,且此時再也不須要調用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方法。
Generator函數還有一個特色,它能夠在函數體外拋出錯誤,而後在函數體內捕獲。
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw 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捕獲,而後Generator函數執行完成,因而第二個錯誤被函數體外的catch捕獲。
注意,上面代碼的錯誤,是用Iterator對象的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代碼塊,那麼Iterator對象的throw方法拋出的錯誤,不影響下一次遍歷,不然遍歷直接終止。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
g.throw();
} catch (e) {
g.next();
}
// hello
上面代碼只輸出hello就結束了,由於第二次調用next方法時,遍歷器狀態已經變成終止了。可是,若是使用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方法,都取到了正確的操做。
這種函數體內捕獲錯誤的機制,大大方便了對錯誤的處理。若是使用回調函數的寫法,想要捕獲多個錯誤,就不得不爲每一個函數寫一個錯誤處理語句。
foo('a', function (a) {
if (a.error) {
throw new Error(a.error);
}
foo('b', function (b) {
if (b.error) {
throw new Error(b.error);
}
foo('c', function (c) {
if (c.error) {
throw new Error(c.error);
}
console.log(a, b, c);
});
});
});
使用Generator函數能夠大大簡化上面的代碼。
function* g(){
try {
var a = yield foo('a');
var b = yield foo('b');
var c = yield foo('c');
} catch (e) {
console.log(e);
}
console.log(a, b, c);
}
反過來,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已經運行結束了。
yield*
語句若是yield命令後面跟的是一個Iterator對象,須要在yield命令後面加上星號,代表它返回的是一個Iterator對象。這被稱爲 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
語句獲得的值,是一個Iterator對象,因此要用星號表示。運行結果就是使用一個Iterator對象,遍歷了多個Generator函數,有遞歸的效果。
yield*
語句等同於在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*
不過是for…of的一種簡寫形式,徹底能夠用後者替代前者。
若是 yield*
後面跟着一個數組,因爲數組原生支持Iterator對象,所以就會遍歷數組成員。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代碼中,yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的Iterator對象。
若是被代理的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(); //
it.next(); //
it.next(); //
it.next(); // "v: foo"
it.next(); //
上面代碼在第四次調用next方法的時候,屏幕上會有輸出,這是由於函數foo的return語句,向函數bar提供了返回值。
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)遍歷函數。
// 因爲返回的是一個Iterator對象,因此要用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']
若是一個對象的屬性是Generator函數,能夠簡寫成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
上面代碼中,myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個Generator函數。
它的完整形式以下,與上面的寫法是等價的。
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
這一節討論一種特殊狀況:構造函數是Generator函數。
function* F(){
yield this.x = 2;
yield this.y = 3;
}
上面代碼中,函數F是一個構造函數,又是一個Generator函數。這時,使用new命令就沒法生成F的實例了,由於F返回的是一個Iterator對象。
'next' in (new F()) // true
那麼,這個時候怎麼生成對象實例呢?
咱們知道,若是構造函數調用時,沒有使用new命令,那麼內部的this對象,綁定當前構造函數所在的對象(好比window對象)。所以,能夠生成一個空對象,使用bind方法綁定F內部的this。這樣,構造函數調用之後,這個空對象就是F的實例對象了。
var obj = {};
var f = F.bind(obj)();
f.next();
f.next();
f.next();
console.log(obj); // { x: 2, y: 3 }
上面代碼中,首先是F內部的this對象綁定obj對象,而後調用它,返回一個Iterator對象。這個對象執行三次next方法(由於F內部有兩個yield語句),完成F內部全部代碼的運行。這時,全部內部屬性都綁定在obj對象上了,所以obj對象也就成了F的實例。
ES7在數組推導的基礎上,提出了Generator函數推導(Generator comprehension)。
let generator = function* () {
for (let i = 0; i < 6; i++) {
yield i;
}
}
let squared = ( for (n of generator()) n * n );
// 等同於
// let squared = Array.from(generator()).map(n => n * n);
console.log(...squared); // 0 1 4 9 16 25
「推導」這種語法結構,不只能夠用於數組,ES7將其推廣到了Generator函數。for…of循環會自動調用Iterator對象的next方法,將返回值的value屬性做爲數組的一個成員。
Generator函數推導是對數組結構的一種模擬,它的最大優勢是惰性求值,即直到真正用到時纔會求值,這樣能夠保證效率。請看下面的例子。
let bigArray = new Array(100000);
for (let i = 0; i < 100000; i++) {
bigArray[i] = i;
}
let first = bigArray.map(n => n * n)[0];
console.log(first);
上面例子遍歷一個大數組,可是在真正遍歷以前,這個數組已經生成了,佔用了系統資源。若是改用Generator函數推導,就能避免這一點。下面代碼只在用到時,纔會生成一個大數組。
let bigGenerator = function* () {
for (let i = 0; i < 100000; i++) {
yield i;
}
}
let squared = ( for (n of bigGenerator()) n * n );
console.log(squared.next());
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) {
yield _;
console.log('Tick!');
yield _;
console.log('Tock!');
}
};
上面的Generator實現與ES5實現對比,能夠看到少了用來保存狀態的外部變量ticking,這樣就更簡潔,更安全(狀態不會被非法篡改)、更符合函數式編程的思想,在寫法上也更優雅。Generator之因此能夠不用外部變量保存狀態,是由於它自己就包含了一個狀態信息,即目前是否處於暫停態。
協程(coroutine)是一種程序運行的方式,能夠理解成「協做的線程」或「協做的函數」。協程既能夠用單線程實現,也能夠用多線程實現。前者是一種特殊的子例程,後者是一種特殊的線程。
傳統的「子例程」(subroutine)採用堆棧式「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。
從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代價,實現多任務的並行。
不難看出,協程適合用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文、能夠分享全局變量。它們的不一樣之處在於,同一時間能夠有多個線程處於運行狀態,可是運行的協程只能有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。
因爲ECMAScript是單線程語言,只能保持一個調用棧。引入協程之後,每一個任務能夠保持本身的調用棧。這樣作的最大好處,就是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。
Generator函數是ECMAScript 6對協程的實現,但屬於不徹底實現。Generator函數被稱爲「半協程」(semi-coroutine),意思是隻有Generator函數的調用者,才能將程序的執行權還給Generator函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。
若是將Generator函數看成協程,徹底能夠將多個須要互相協做的任務寫成Generator函數,它們之間使用yield語句交換控制權。
Generator能夠暫停函數執行,返回任意表達式的值。這種特色使得Generator有多種應用場景。
Generator函數的暫停執行的效果,意味着能夠把異步操做寫在yield語句裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了,由於異步操做的後續操做能夠放在yield語句下面,反正要等到調用next方法時再執行。因此,Generator函數的一個重要實際意義就是用來處理異步操做,改寫回調函數。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼表示,第一次調用loadUI函數時,該函數不會執行,僅返回一個Iterator對象。下一次對該Iterator對象調用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語句能夠手動逐行讀取文件。
若是有一個多步操做很是耗時,採用回調函數,可能會寫成下面這樣。
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
採用Promise改寫上面的代碼。
Q.fcall(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() {
try {
var value1 = yield step1();
var value2 = yield step2(value1);
var value3 = yield step3(value2);
var value4 = yield step4(value3);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
而後,使用一個函數,按次序自動執行全部步驟。
scheduler(longRunningTask());
function scheduler(task) {
setTimeout(function() {
var taskObj = task.next(task.value);
// 若是Generator函數未結束,就繼續調用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}, 0);
}
注意,yield語句是同步運行,不是異步運行(不然就失去了取代回調函數的設計目的了)。實際操做中,通常讓yield語句返回Promise對象。
var Q = require('q');
function delay(milliseconds) {
var deferred = Q.defer();
setTimeout(deferred.resolve, milliseconds);
return deferred.promise;
}
function* f(){
yield delay(100);
};
上面代碼使用Promise的函數庫Q,yield語句返回的就是一個Promise對象。
多個任務按順序一個接一個執行時,yield語句能夠按順序排列。多個任務須要並列執行時(好比只有A任務和B任務都執行完,才能執行C任務),能夠採用數組的寫法。
function* parallelDownloads() {
let [text1,text2] = yield [
taskA(),
taskB()
];
console.log(text1, text2);
}
上面代碼中,yield語句的參數是一個數組,成員就是兩個任務taskA和taskB,只有等這兩個任務都完成了,纔會接着執行下面的語句。
利用Generator函數,能夠在任意對象上部署Iterable接口。
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函數,就有了Iterable接口。
下面是一個對數組部署Iterable接口的例子,儘管數組原生具備這個接口。
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
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使得數據或者操做,具有了相似數組的接口。
尾調用(Tail Call)是函數式編程的一個重要概念,就是指某個函數的最後一步是調用另外一個函數。
function f(x){
return g(x);
}
//下面三種狀況都不是尾調用
function f(x){
let y = g(x);
return y;
}
function f(x){
return g(x) + 1;
}
function f(x){
g(x);
}
最後一種狀況等同於下面的代碼。
function f(x){
g(x);
return undefined;
}
尾調用不必定出如今函數尾部,只要是最後一步操做便可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
上面代碼中,函數m和n都屬於尾調用,由於它們都是函數f的最後一步操做。
咱們知道,函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)。
尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就能夠了。
「尾調用優化」(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,這將大大節省內存。這就是「尾調用優化」的意義。
注意,只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行「尾調用優化」。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
上面的函數不會進行尾調用優化,由於內層函數inner用到了,外層函數addOne的內部變量one。
函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。
遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生「棧溢出」錯誤。
尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) //120
對上面的遞歸優化
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
函數式編程有一個概念,叫作柯里化(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
遞歸本質上是一種循環操做。純粹的函數式編程語言沒有循環操做命令,全部的循環都用遞歸實現,這就是爲何尾遞歸對這些語言極其重要。
箭頭函數能夠綁定this對象,大大減小了顯式綁定this對象的寫法(call、apply、bind)。可是,箭頭函數並不適用於全部場合,因此ES7提出了「函數綁定」(function bind)運算符,用來取代call、apply、bind調用。雖然該語法仍是ES7的一個提案,可是Babel轉碼器已經支持。
函數綁定運算符是並排的兩個雙引號(::),雙引號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,做爲上下文環境(即this對象),綁定到右邊的函數上面。
在ES6以前,社區制定了一些模塊加載方案,最主要的有CommonJS和AMD兩種。前者用於服務器,後者用於瀏覽器。ES6在語言規格的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代現有的CommonJS和AMD規範,成爲瀏覽器和服務器通用的模塊解決方案。
ES6模塊的設計思想,是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時肯定這些東西。好比,CommonJS模塊就是對象,輸入時必須查找對象屬性。ES6模塊不是對象,而是經過export命令顯式指定輸出的代碼,輸入時也採用靜態命令的形式。因此,ES6能夠在編譯時就完成模塊編譯,效率要比CommonJS模塊高。
模塊功能由三個命令構成:export,import和module。export命令用於用戶自定義模塊,規定對外接口;import命令用於輸入其餘模塊提供的功能,同時創造命名空間(namespace),防止函數名衝突;module用於總體輸入其它模塊的提供的功能。
簡單實例
// lib/math.js
export function sum(x, y) {
return x + y;
}
export var pi = 3.141593;
// app.js
import * as math from "lib/math";
console.log(math.pi);
ES6容許將獨立的JS文件做爲模塊,也就是說,容許一個JavaScript腳本文件調用另外一個腳本文件。該文件內部的全部變量、函數、類,外部沒法獲取,必須使用export關鍵字輸出,一種輸出方式是隻須要在原有聲明變量、函數、類語句前加export,另外一種方式是在export後使用大括號指定須要輸出的變量、函數、類,而且中間用逗號分隔。下面是一個JS文件,裏面使用export命令輸出變量。
// profile.js
export var name = 'Michael';
export var year = 1958;
另一種寫法。
// profile.js
var name = 'Michael';
var year = 1958;
export {name, year};
上面代碼在export命令後面,使用大括號指定所要輸出的一組變量。它與前一種寫法(直接放置在var語句前)是等價的,可是應該優先考慮使用這種寫法。由於這樣就能夠在腳本尾部,一眼看清楚輸出了哪些變量。
使用export命令定義了模塊的對外接口之後,其餘JS文件就能夠經過import命令加載這個模塊(文件)。
// main.js
import {name, year} from './profile';
function setHeader(element) {
element.textContent = name;
}
上面代碼屬於另外一個文件main.js,import命令就用於加載profile.js文件,並從中輸入變量。import命令接受一個對象(用大括號表示),裏面指定要從其餘模塊導入的變量名。大括號裏面的變量名,必須與被導入模塊(profile.js)對外接口的名稱相同。
若是想爲輸入的變量從新取一個名字,import語句中要使用as關鍵字,將輸入的變量重命名。
import { name as nickName } from './profile';
ES6支持多重加載,即所加載的模塊中又加載其餘模塊。
import { Vehicle } from './Vehicle';
class Car extends Vehicle {
move () {
console.log(this.name + ' is spinning wheels...')
}
}
export { Car }
若是在一個模塊之中,先輸入後輸出同一個模塊,import語句能夠與export語句寫在一塊兒。
export { es6 as default } from './someModule';
// 等同於
import { es6 } from './someModule';
export default es6;
上面代碼中,export和import語句能夠結合在一塊兒,寫成一行。可是從可讀性考慮,不建議採用這種寫法,應該採用標準寫法。
下面是一個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文件輸入circle.js模塊。
// main.js
import * as circle from 'circle';
console.log("圓面積:" + circle.area(4));
console.log("圓周長:" + circle.circumference(14));
module命令能夠取代import語句,達到總體輸入模塊的做用。
// main.js
module circle from 'circle';
console.log("圓面積:" + circle.area(4));
console.log("圓周長:" + circle.circumference(14));
module命令後面跟一個變量,表示輸入的模塊定義在該變量上。
使用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,在模塊外部是無效的。加載的時候,視同匿名函數加載。
下面比較一下默認輸出和正常輸出。
import crc32 from 'crc32';
// 對應的輸出
export default function crc32(){}
// 須要使用大括號
import { crc32 } from 'crc32';
// 對應的輸出
export function crc32(){};
export default
命令用於指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,所以export deault
命令只能使用一次。因此,import命令後面纔不用加大括號,由於只可能對應一個方法。
本質上, export default
就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。因此,下面的寫法是有效的。
// modules.js
export default function (x, y) {
return x * y;
};
// app.js
import { default } from 'modules';
有了 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();
模塊之間也能夠繼承。
假設有一個circleplus模塊,繼承了circle模塊。
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
上面代碼中的「export *」,表示輸出circle模塊的全部屬性和方法,export default命令定義模塊的默認方法。
這時,也能夠將circle的屬性或方法,更名後再輸出。
// circleplus.js
export { area as circleArea } from 'circle';
上面代碼表示,只輸出circle模塊的area方法,且將其更名爲circleArea。
加載上面模塊的寫法以下。
// main.js
module math from "circleplus";
import exp from "circleplus";
console.log(exp(math.e));
上面代碼中的 import exp
表示,將circleplus模塊的默認方法加載爲exp方法。
瀏覽器目前還不支持ES6模塊,爲了如今就能使用,能夠將轉爲ES5的寫法。
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
另外一種解決方法是使用 SystemJS 。它是一個墊片庫(polyfill),能夠在瀏覽器內加載ES6模塊、AMD模塊和CommonJS模塊,將其轉爲ES5格式。它在後臺調用的是Google的Traceur轉碼器。
使用時,先在網頁內載入system.js文件。
<script src="system.js"></script>
而後,使用 System.import
方法加載模塊文件。
<script>
System.import('./app');
</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>
ECMA-262 第 3 版引入了 try-catch 語句,做爲 JavaScript 中處理異常的一種標準方式。基本的語法以下所示,顯而易見,這與 Java 中的 try-catch 語句是徹底相同的。
try{
// 可能會致使錯誤的代碼
} catch(error){
// 在錯誤發生時怎麼處理
}
也就是說,咱們應該把全部可能會拋出錯誤的代碼都放在 try 語句塊中,而把那些用於錯誤處理的代碼放在 catch 塊中。若是 try 塊中的任何代碼發生了錯誤,就會當即退出代碼執行過程,而後接着執行 catch 塊。此時, catch 塊會接收到一個包含錯誤信息的對象。
內置使用內置的Error對象具備兩個標準屬性name和message
使用 try-catch 最適合處理那些咱們沒法控制的錯誤。假設你在使用一個大型 JavaScript 庫中的函數,該函數可能會有意無心地拋出一些錯誤。因爲咱們不能修改這個庫的源代碼,因此大可將對該函數的調用放在 try-catch 語句當中,萬一有什麼錯誤發生,也好恰當地處理它們。
在明明白白地知道本身的代碼會發生錯誤時,再使用 try-catch 語句就不太合適了。例如,若是傳遞給函數的參數是字符串而非數值,就會形成函數出錯,那麼就應該先檢查參數的類型,而後再決定如何去作。在這種狀況下,不該用使用 try-catch 語句。
finally都是可選的,但 finally 子句一經使用,其代碼不管如何都會執行。換句話說, try 語句塊中的代碼所有正常執行, finally 子句會執行;若是由於出錯而執行了 catch 語句塊, finally 子句照樣還會執行。
function fn(){
try {
var x = 1;
throw new Error('error');
} catch (e) {
console.log('x=' + x);
return x;
} finally {
x = 2;
console.log('x=' + x);
}
}
上面代碼說明,即便有return語句在前,finally代碼塊依然會獲得執行,且在其執行完畢後,並不影響return語句要返回的值。
ECMA-262 定義了下列 7 種錯誤類型: Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError
若是沒有把 eval() 當成函數調用,就會拋出EvalError錯誤。一些瀏覽器不會正確拋出這個錯誤。
RangeError是當一個值超出有效範圍時發生的錯誤。主要有幾種狀況,一是數組長度爲負數,二是Number對象的方法參數超出範圍,以及函數堆棧超過最大值。
new Array(-1);
(1234).toExponential(21);
new Array(Number.MAX_VALUE);
ReferenceError是引用一個不存在的變量時發生的錯誤。另外一種觸發場景是,將一個值分配給沒法分配的對象,好比對函數的運行結果或者this賦值。
undefinedVar;
console.log() = 1;
this = 1;
SyntaxError是解析代碼時發生的語法錯誤。
// 變量名錯誤
var 1a;
// 缺乏括號
console.log 'hello');
TypeError是變量或參數不是預期類型時發生的錯誤。好比,對字符串、布爾值、數值等原始類型的值使用new命令,就會拋出這種錯誤,由於new命令的參數應該是一個構造函數。訪問不存在的方法時也會拋出該錯誤。
new 123
var obj = {};
obj.unknownMethod()
URIError是URI相關函數的參數不正確時拋出的錯誤,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()這六個函數。
decodeURI('%2')
與 try-catch 語句相配的還有一個 throw 操做符,用於隨時拋出自定義錯誤。拋出錯誤時,必需要給 throw 操做符指定一個值,這個值是什麼類型,沒有要求。下列代碼都是有效的。
throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript"};
在遇到 throw 操做符時,代碼會當即中止執行。僅當有 try-catch 語句捕獲到被拋出的值時,代碼纔會繼續執行。
經過使用某種內置錯誤類型,能夠更真實地模擬瀏覽器錯誤。每種錯誤類型的構造函數接收一個參數,即實際的錯誤消息。下面是一個例子。
throw new Error("error");
這行代碼拋出了一個通用錯誤,帶有一條自定義錯誤消息。瀏覽器會像處理本身生成的錯誤同樣,來處理這行代碼拋出的錯誤。
在建立自定義錯誤消息時最經常使用的錯誤類型是 Error 、 RangeError 、 ReferenceError 和 TypeError 。
另外,利用原型鏈還能夠經過繼承 Error 來建立自定義錯誤類型
function UserError(message) {
this.message = message || "默認信息";
this.name = "UserError";
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
瀏覽器對待繼承自 Error 的自定義錯誤類型,就像對待其餘錯誤類型同樣。若是要捕獲本身拋出的錯誤而且把它與瀏覽器錯誤區別對待的話,建立自定義錯誤是頗有用的。
要針對函數爲何會執行失敗給出更多信息,拋出自定義錯誤是一種很方便的方式。應該在出現某種特定的已知錯誤條件,致使函數沒法正常執行時拋出錯誤。換句話說,瀏覽器會在某種特定的條件下執行函數時拋出錯誤。
說到拋出錯誤與捕獲錯誤,咱們認爲只應該捕獲那些你確切地知道該如何處理的錯誤。捕獲錯誤的目的在於避免瀏覽器以默認方式處理它們;而拋出錯誤的目的在於提供錯誤發生具體緣由的消息。
任何沒有經過 try-catch 處理的錯誤都會觸發 window 對象的 error 事件。在任何 Web 瀏覽器中, onerror 事件處理程序都不會建立 event 對象,但它能夠接收三個參數:錯誤消息、錯誤所在的 URL 和行號。多數狀況下,只有錯誤消息有用,由於 URL 只是給出了文檔的位置,而行號所指的代碼行既可能出自嵌入的 JavaScript 代碼,也可能出自外部的文件。
只要發生錯誤,不管是否是瀏覽器生成的,都會觸發 error 事件,並執行這個事件處理程序。而後,瀏覽器默認的機制發揮做用,像往常同樣顯示出錯誤消息。像下面這樣在事件處理程序中返回false ,能夠阻止瀏覽器報告錯誤的默認行爲。
window.onerror = function(message, url, line){
alert(message);
return false;
};
經過返回 false ,這個函數實際上就充當了整個文檔中的 try-catch 語句,能夠捕獲全部無代碼處理的運行時錯誤。這個事件處理程序是避免瀏覽器報告錯誤的最後一道防線,理想狀況下,只要可能就不該該使用它。只要可以適當地使用 try-catch 語句,就不會有錯誤交給瀏覽器,也就不會觸發error 事件。
圖像也支持 error 事件。只要圖像的 src 特性中的 URL 不能返回能夠被識別的圖像格式,就會觸發 error 事件。
錯誤處理的核心,是首先要知道代碼裏會發生什麼錯誤。因爲 JavaScript 是鬆散類型的,並且也不會驗證函數的參數,所以錯誤只會在代碼運行期間出現。通常來講,須要關注三種錯誤:
類型轉換錯誤發生在使用某個操做符,或者使用其餘可能會自動轉換值的數據類型的語言結構時。在使用相等(==)和不相等(!=)操做符,或者在 if 、 for 及 while 等流控制語句中使用非布爾值時, 最常發生類型轉換錯誤。強烈建議使用全等操做符(===,!==)。
if (str3){ //絕對不要這樣!!!
}
if (typeof str3 == "string"){//合理的比較
}
JavaScript 是鬆散類型的,也就是說,在使用變量和函數參數以前,不會對它們進行比較以確保它們的數據類型正確。爲了保證不會發生數據類型錯誤,只能依靠開發人員編寫適當的數據類型檢測代碼。在將預料以外的值傳遞給函數的狀況下,最容易發生數據類型錯誤。大致上來講,基本類型的值應該使用 typeof 來檢測,而對象的值則應該使用 instanceof 來檢測。
JavaScript 與服務器之間的任何一次通訊,都有可能會產生錯誤。
第一種通訊錯誤與格式不正確的 URL 或發送的數據有關。最多見的問題是在將數據發送給服務器以前,沒有使用 encodeURIComponent() 對數據進行編碼。
對於查詢字符串,應該記住必需要使用 encodeURIComponent() 方法。爲了確保這一點,有時候能夠定義一個處理查詢字符串的函數,例如:
function addQueryStringArg(url, name, value){
if (url.indexOf("?") == -1){
url += "?";
} else {
url += "&";
}
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
能夠把錯誤回寫到服務器,標明來自前端。
function logError(sev, msg){
var img = new Image();
img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" +
encodeURIComponent(msg);
}
這個 logError() 函數接收兩個參數:表示嚴重程度的數值或字符串(視所用系統而異)及錯誤消息。其中,使用了 Image 對象來發送請求,這樣作很是靈活,主要表現以下幾方面。
引用類型的值是保存在內存中的對象。與其餘語言不一樣,JavaScript不容許直接訪問內存中的位置,也就是說不能直接操做對象的內存空間。在操做對象時,其實是在操做對象的引用而不是實際的對象(和Java相似)。爲此,引用類型的值是按引用訪問的
不少語言中,字符串以對象的形式來表示,所以被認爲是引用類型的。ECMAScript放棄了這一傳統。
ECMAScript 變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值。基本類型值指的是簡單的數據段,而引用類型值指那些可能由多個值構成的對象。
咱們不能給基本類型的值添加屬性,儘管這樣作不會致使任何錯誤
ECMAScript 中全部函數的參數都是按值傳遞的
當從一個變量向另外一個變量複製引用類型的值時,一樣也會將存儲在變量對象中的值複製一份放到爲新變量分配的空間中。不一樣的是,這個值的副本其實是一個指針,而這個指針指向存儲在堆中的一個對象。複製操做結束後,兩個變量實際上將引用同一個對象。
在向參數傳遞基本類型的值時,被傳遞的值會被複制給一個局部變量(即命名參數,或者用ECMAScript的概念來講,就是 arguments 對象中的一個元素)。在向參數傳遞引用類型的值時,會把這個值在內存中的地址複製給一個局部變量,所以這個局部變量的變化會反映在函數的外部
若是使用instanceof 操做符檢測基本類型的值,則該操做符始終會返回false,由於基本類型不是對象。
若是變量的值是一個對象或null,則typeof操做符會返回」object」
ECMA-262規定任何在內部實現[[Call]]方法的對象都應該在應用 typeof 操做符時返回」function」。因爲Safari 5及以前版本和Chrome 7及以前版本瀏覽器中的正則表達式也實現了這個方法,所以對正則表達式應用 typeof 會返回」function」。在IE和Firefox中,對正則表達式應用typeof會返回」object」。
標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止(若是找不到標識符,一般會致使錯誤發生)。
執行環境定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中。雖然咱們編寫的代碼沒法訪問這個對象,但解析器在處理數據時會在後臺使用它。 全局執行環境是最外圍的一個執行環境。根據 ECMAScript實現所在的宿主環境不一樣,表示執行環境的對象也不同。在Web瀏覽器中,全局執行環境被認爲是 window 對象,所以全部全局變量和函數都是做爲window對象的屬性和方法建立的。某個執行環境中的全部代碼執行完畢後,該環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬(全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時纔會被銷燬)。 每一個函數都有本身的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境。ECMAScript 程序中的執行流正是由這個方便的機制控制着。 當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(scope chain)。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則將其活動對象(activation object)做爲變量對象。活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。做用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。
內部環境能夠經過做用域鏈訪問全部的外部環境,但外部環境不能訪問內部環境中的任何變量和函數。這些環境之間的聯繫是線性、有次序的。每一個環境均可以向上搜索做用域鏈,以查詢變量和函數名;但任何環境都不能經過向下搜索做用域鏈而進入另外一個執行環境。
var color = 'blue';
function changeColor(){
function swapColors(){
}
}
做用域鏈中包含 swapColors->changeColor->window三個對象
當執行流進入下列任何一個語句時,做用域鏈就會獲得加長:
這兩個語句都會在做用域鏈的前端添加一個變量對象。對with語句來講,會將指定的對象添加到做用域鏈中。對catch語句來講,會建立一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明
function buildUrl(){
var qs = '?debug=true';
with(location) {
var url = href + qs;
}
return url;
}
with 語句接收的是location 對象,所以其變量對象中就包含了location 對象的全部屬性和方法,而這個變量對象被添加到了做用域鏈的前端
聲明變量在函數內部,最接近的環境就是函數的局部環境;在with語句中,最接近的環境是函數環境。若是初始化變量時沒有使用var聲明,該變量會自動被添加到全局環境。
嚴格模式下,初始化未經聲明的變量會致使錯誤。建議仍是要用var聲明變量,固然能夠邊聲明邊初始化。
查詢標識符搜索過程從做用域鏈的前端開始,向上逐級查詢與給定名字匹配的標識符。
若是局部環境中存在着同名標識符,就不會使用位於父環境中的標識符
var color = 'blue';
function getColor(){
var color = 'red';
return color;
}
alert(getColor());
位於局部變量 color 的聲明以後的代碼,若是不使用 window.color 都沒法訪問全局 color變量。
ECMASctipt6中let實際上爲JavaScript增長了塊級做用域。另外,ES6也規定,函數自己的做用域,在其所在的塊級做用域以內。
function f() { console.log('I am outside!'); }
(function () {
if(false) {
// 重複聲明一次函數f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代碼在ES5中運行,會獲得「I am inside!」,可是在ES6中運行,會獲得「I am outside!」。這是由於ES5存在函數提高,無論會不會進入if代碼塊,函數聲明都會提高到當前做用域的頂部,獲得執行;而ES6支持塊級做用域,無論會不會進入if代碼塊,其內部聲明的函數皆不會影響到做用域的外部。
須要注意的是,若是在嚴格模式下,函數只能在頂層做用域和函數內聲明,其餘狀況(好比if代碼塊、循環代碼塊)的聲明都會報錯。
JavaScript 中最經常使用的垃圾收集方式是標記清除(mark-and-sweep)。
另外一種不太常見的垃圾收集策略叫作引用計數(reference counting)。
IE中的COM對象的垃圾收集機制採用的就是引用計數策略,只要在IE中涉及COM對象,就會存在循環引用的問題
循環引用指的是對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用。即便A、B再也不被使用,可是因爲其引用計數不爲0,並不會被釋放。
IE9把BOM和DOM對象都轉換成了真正的JavaScript對象。
垃圾收集器是週期性運行的,並且若是爲變量分配的內存數量很可觀,那麼回收工做量也是至關大的。
一旦數據再也不有用,最好經過將其值設置爲 null 來釋放其引用——這個作法叫作解除引用(dereferencing)。這一作法適用於大多數全局變量和全局對象的屬性。局部變量會在它們離開執行環境時自動被解除引用
From: http://howiefh.github.io/2015/08/28/javascript-grammar/