第一部分 型別和語法
第一章 型別
JavaScript 有七種內置型別:
? 空值(null)
? 未定義(undefined)
? 布林值( boolean)
? 數字(number)
? 字串(string)
? 物件(object)
? 符號(symbol,ES6 中新增)
typeof undefined === "undefined"; // true
typeof true === "boolean"; // true
typeof 42 === "number"; // true
typeof "42" === "string"; // true
typeof { life: 42 } === "object"; // true
// ES6中新加入的型別
typeof Symbol() === "symbol"; // true
typeof null === "object"; // true
// 使用復合條件來檢測 null 值的型別
var a = null;
(!a && typeof a === "object"); // true
typeof function a(){ /* .. */ } === "function"; // true 是 object 的一個“子型別”
typeof [1,2,3] === "object"; // true 也是 object 的一個“子型別”
JavaScript 中的變數是沒有型別的,只有值才有,變數可以隨時持有任何型別的值,
已在作用域中宣告但還沒有賦值的變數,是 undefined 的,相反,還沒有在作用域中宣告過的變數,是 undeclared 的,
直接呼叫 undefined 的變數不會報錯,但是直接呼叫 undeclared 的會報錯,所以判斷變數的 typeof 比直接判斷變數更安全;如下;
var a;
if (a) {} //這里會報錯;
if (typeof a === undefined) {} // 這里不會報錯
if (window.a) {} // 這種方式也可以
第二章 值
陣列
字串鍵值能夠被強制型別轉換為十進制數字的話,它就會被當作數字索引來處理,如
var a = [];
a["123"] = 23;
a.length;// 124
類陣列轉換成陣列:
- Array.prototype.slice.call(arguments);
- Array.from(arguements);
字串
JavaScript 中字串是不可變的,而陣列是可變的,
字串不可變是指字串的成員函式不會改變其原始值,而是創建并回傳一個新的字串,而陣列的成員函式都是在其原始值上進行操作,
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
// 字串反轉
var c = a
// 將a的值轉換為字符陣列
.split( "" )
// 將陣列中的字符進行倒轉
.reverse()
// 將陣列中的字符拼接回字串
.join( "" );
數字
JavaScript 只有一種數值型別:number(數字),包括“整數”和帶小數的十進制數,
// tofixed指定小數位數
var a = 1.234
a.toFixed(0);// 1;
a.toFixed(4);// 1.2340
// toPrecision(..) 方法用來指定有效數位的顯示位數
var a = 42.59;
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
a.toPrecision( 6 ); // "42.5900"
42.toFixed( 3 ); // SyntaxError
// 下面的語法都有效:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42 .toFixed(3); // "42.000" 注意其中的空格
var onethousand = 1E3; // 即 1 * 10^3
var onemilliononehundredthousand = 1.1E6; // 即 1.1 * 10^6
0xf3; // 243的十六進制
0Xf3; // 同上
0363; // 243的八進制
// 從 ES6 開始,嚴格模式(strict mode)不再支持 0363 八進制格式(新格式如
//下),0363 格式在非嚴格模式(non-strict mode)中仍然受支持,但是考慮到
//將來的兼容性,最好不要再使用(我們現在使用的應該是嚴格模式),
0o363; // 243的八進制
0O363; // 同上
0b11110011; // 243的二進制
0B11110011; // 同上
0.1 + 0.2 === 0.3 // false 合適因為在js中,二進制浮點數不是十分精確的
console.log(.1 + .2); // 0.30000000000000004
如何解決浮點數不精確的問題:最常見的方法是設定一個誤差范圍值,通常稱為“機器精度”(machine epsilon),對 JavaScript 的數字來說,這個值通常是 2^-52 (2.220446049250313e-16),從 ES6 開始,該值定義在 Number.EPSILON 中;
// polyfill
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
function numbersCloseEnoughToEqual(n1,n2) {
return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false
PS:會有誤差的原因是:計算機是通過二進制的方式存盤資料的,在相加的時候,是拿二進制去相加,0.1 的二進制是 0.0001100110011001100...(1100 回圈),0.2 的二進制是:0.00110011001100...(1100 回圈);在 JavaScript 的 Number 實作遵循 IEEE 754 標準,使用 64 位固定長度來表示,也就是標準的 double 雙精度浮點數,在二進制科學表示法中,雙精度浮點數的小數部分最多只能保留 52 位,再加上前面的 1,其實就是保留 53 位有效數字,剩余的需要舍去,遵從“0 舍 1 入”的原則,
JS 中能夠被最大呈現的整數為:2^53 - 1,即 9007199254740991,在 ES6 中被定義為 Number.MAX_SAFE_INTEGER,最小整數是 -9007199254740991,在 ES6 中被定義為 Number.MIN_SAFE_INTEGER,如果需要精確呈現,需要將其轉成 string;
整數檢測:
// es6 是否是整數
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false
// polyfill
if (!Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
};
}
// es6 是否是安全的整數
Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true
Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true
// 最大安全數為什么不安全?
2**53 //9007199254740992
2**53 + 1 //9007199254740992
2**53 + 2 //9007199254740994
2**53 + 3 //9007199254740996
2**53 + 4 //9007199254740996
// polyfill
if (!Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) &&
Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
};
}
undefined 指從未賦值
null 指曾賦過值,但是目前沒有值
null 是一個特殊關鍵字,不是識別符號,我們不能將其當作變數來使用和賦值,然而 undefined 卻是一個識別符號,可以被當作變數來使用和賦值
void 運算子:它的值為 undefined
var a = 42;
console.log( void a, a ); // undefined 42
NaN
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true——暈! 只要用數值除以某個變數,這個變數就是NaN
2/{};
isNaN({})// true;
// es6有Number.isNaN
// polyfill
if (!Number.isNaN) {
Number.isNaN = function(n) {
return (
typeof n === "number" &&
window.isNaN( n )
);
};
}
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false——好!
// 還有個更為簡單的方法 即利用 NaN 不等于自身這個特點
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
};
}
無窮數
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
a = Number.MAX_VALUE;
// 就近取整模式
console.log(a); // 1.7976931348623157e+308
console.log(a * 2); // Infinity
console.log(a + Math.pow( 2, 969 )); // 1.7976931348623157e+308
console.log(Infinity / Infinity); // NaN
1/Infinity // 0
1/-Infinity //-0
Infinity / 1 //Infinity
-Infinity / 1 //-Infinity
零值
// 加法和減法運算不會得到負零(negative zero)
var a = 0 / -3;
// 至少在某些瀏覽器的控制臺中顯示是正確的
console.log(a); // -0
// 但是規范定義的回傳結果是這樣!
console.log(a.toString()); // "0"
console.log(a + ""); // "0"
console.log(String( a )); // "0"
// JSON也如此,很奇怪
console.log(JSON.stringify( a )); // "0"
console.log(+"-0"); // -0
console.log(Number( "-0") ); // -0
console.log(JSON.parse( "-0" )); // -0 JSON.stringify(-0) 回傳 "0",而 JSON.parse("-0") 回傳 -0,
var a = 0;
var b = 0 / -3;
console.log(a == b); // true
console.log(-0 == 0); // true
console.log(a === b); // true
console.log(-0 === 0); // true
console.log(0 > -0); // false
console.log(a > b); // false
// 是否是-0
function isNegZero(n) {
n = Number( n );
return (n === 0) && (1 / n === -Infinity);
}
isNegZero( -0 ); // true
isNegZero( 0 / -3 ); // true
isNegZero( 0 ); // false
特殊等式 Object.is
var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false
// polyfill
if (!Object.is) {
Object.is = function(v1, v2) {
// 判斷是否是-0
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// 判斷是否是NaN
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情況
return v1 === v2;
};
}
簡單值通過值復制的方式來賦值 / 傳遞,包括 null、undefined、字串、數字、布爾和 ES6 中的 symbol,
復合值(compound value)——物件(包括陣列和封裝物件)和函式,則總是通過參考復制的方式來賦值 / 傳遞,
第三章 原生函式
內部屬性 [[Class]]
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"
Object.prototype.toString.call( null );
// "[object Null]"
Object.prototype.toString.call( undefined );
// "[object Undefined]"
Object.prototype.toString.call( "abc" );
// "[object String]"
Object.prototype.toString.call( 42 );
// "[object Number]"
Object.prototype.toString.call( true );
// "[object Boolean]"
封裝物件包裝: 由于基本型別值沒有 .length 和 .toString() 這樣的屬性和方法,需要通過封裝物件才能訪問,此時 JavaScript 會自動為基本型別值包裝(box 或者 wrap)一個封裝物件;
拆封:得到封裝物件中的基本型別值,可以使用 valueOf() 函式;
建構式 Array(..) 不要求必須帶 new 關鍵字,不帶時,它會被自動補上,因此 Array(1,2,3) 和 new Array(1,2,3) 的效果是一樣的,
稀疏陣列:將包含至少一個“空單元”的陣列;
var a = new Array( 3 );
var b = [ undefined, undefined, undefined ];
var c = [];
c.length = 3;
a.join( "-" ); // "--"
b.join( "-" ); // "--"
a.map(function(v,i){ return i; }); // [ undefined x 3 ]
b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]
var a = Array.apply( null, { length: 3 } );// 等同于Array(undefined, undefined, undefined)
a; // [ undefined, undefined, undefined ]
永遠不要創建和使用空單元陣列
除非萬不得已,否則盡量不要使用 Object(..)/Function(..)/RegExp(..)
RegExp(..) 有時還是很有用的,比如動態定義正則運算式時
Date(..) 和 Error(..)
// Date.now的polyfill
if (!Date.now) {
Date.now = function(){
return (new Date()).getTime();
};
}
如果呼叫 Date() 時不帶 new 關鍵字,則會得到當前日期的字串值,其具體格式規范沒有規定,瀏覽器使用 "Fri Jul 18 2014 00:31:02 GMT-0500 (CDT)"這樣的格式來顯示,
String#indexOf(..):在字串中找到指定子字串的位置,
String#charAt(..):獲得字串指定位置上的字符,
String#substr(..)、String#substring(..) 和 String#slice(..) :獲得字串的指定部分,
String#toUpperCase() 和 String#toLowerCase():將字串轉換為大寫或小寫,
String#trim():去掉字串前后的空格,回傳新的字串,
以上方法并不改變原字串的值,而是回傳一個新字串,
var a = 'abcde';
a.charAt(1) // 'b'
a.charCodeAt(1) //98
a.concat(1) //'abcde1'
a.endsWith('e') // true
a.indexOf('b') //1
a.includes('b') //true
a.lastIndexOf('d') //3
a.match(/e/g) //['e']
a.repeat()//''
a.repeat(2)//'abcdeabcde'
a.replace('d', 4)//'abc4e'
a.replaceAll('e', '5')//'abcd5'
a.search(/b/)//1
a.slice(0, 1)//'a'
a.split('')//(5) ['a', 'b', 'c', 'd', 'e']
a.startsWith(1)//false
a.substr(0,2) // 'ab' 第一個引數 起始下標 第二引數 長度
a.substring(1,2)//'b' 第一個引數 起始下標 第二個引數 結束下標
'B'.toLowerCase()//'b'
a.toUpperCase()//'ABCDE'
' fd '.trim() //'fd'
第四章 強制型別轉換
型別轉換發生在靜態型別語言的編譯階段,而強制型別轉換則發生在動態型別語言的運行時(runtime),
抽象值操作
toString
- 該方法可重新定義;
- JSON.stringfy 在將 JSON 物件序列化為字串時也用到了 ToString 在物件中遇到 undefined、function 和 symbol 時會自動將其忽略,在陣列中則會回傳 null(以保證單元位置不變),
JSON.stringify(value[, replacer[, space]])
-
- value: 必需, 要轉換的 JavaScript 值(通常為物件或陣列),
- replacer: 可選,用于轉換結果的函式或陣列,
如果 replacer 為函式,則 JSON.stringify 將呼叫該函式,并傳入每個成員的鍵和值,使用回傳值而不是原始值,如果此函式回傳 undefined,則排除成員,根物件的鍵是一個空字串:"",
如果 replacer 是一個陣列,則僅轉換該陣列中具有鍵值的成員,成員的轉換順序與鍵在陣列中的順序一樣, - space: 可選,文本添加縮進、空格和換行符,如果 space 是一個數字,則回傳值文本在每個級別縮進指定數目的空格,如果 space 大于 10,則文本縮進 10 個空格,space 也可以使用非數字,如:\t,
(1) 字串、數字、布林值和 null 的 JSON.stringify(..) 規則與 ToString 基本相同,
(2) 如果傳遞給 JSON.stringify(..) 的物件中定義了 toJSON() 方法,那么該方法會在字串化前呼叫,以便將物件轉換為安全的 JSON 值,
JSON.stringify(..) 并不是強制型別轉換,在這里介紹是因為它涉及 ToString 強制型別轉換
JSON.stringify( undefined ); // undefined
JSON.stringify( function(){} ); // undefined
JSON.stringify(
[1,undefined,function(){},4]
); // "[1,null,null,4]"
JSON.stringify(
{ a:2, b:function(){} }
); // "{"a":2}"
// 對包含回圈參考的物件執行 JSON.stringify(..) 會出錯,
// Uncaught TypeError: Converting circular structure to JSON
// --> starting at object with constructor 'Object'
// | property 'c' -> object with constructor 'Object'
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// 在a中創建一個回圈參考
o.e = a;
// 回圈參考在這里會產生錯誤
// JSON.stringify( a );
// 自定義的JSON序列化 toJSON() 應該“回傳一個能夠被字串化的安全的 JSON 值”,而不是“回傳一個 JSON 字串”,
a.toJSON = function() {
// 序列化僅包含b
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
} );
// "{"b":42,"d":[1,2,3]}"
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, null, 3 );
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"
JSON.stringify( a, null, "-----" );
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"
ToNumber
true 轉換為 1,false 轉換為 0,undefined 轉換為 NaN,null 轉換為 0,
為了將值轉換為相應的基本型別值,抽象操作 ToPrimitive(參見 ES5 規范 9.1 節)會首先檢查該值是否有 valueOf() 方法,如果有并且回傳基本型別值,就使用該值進行強制型別轉換,如果沒有就使用 toString()的回傳值(如果存在)來進行強制型別轉換,如果 valueOf() 和 toString() 均不回傳基本型別值,會產生 TypeError 錯誤,
var a = {
valueOf: function(){
return "42";
}
};
var b = {
toString: function(){
return "42";
}
};
var c = [4,2];
c.toString = function(){
return this.join( "" ); // "42"
};
Number( a ); // 42
Number( b ); // 42
Number( c ); // 42
Number( "" ); // 0
Number( [] ); // 0
Number( [ "abc" ] ); // NaN
ToBoolean
假值:
? undefined
? null
? false
? +0、-0 和 NaN
? ""
假值串列以外的值都是真值,
假值物件(falsy object)
document.all 瀏覽器自帶的來判斷瀏覽器是否是老版本的 IE,
:if(document.all) { /_ it’s IE _/ }
var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );// "''"是真值
顯式強制型別轉換
var a = 42;
var b = String( a );// String(..) 遵循前面講過的 ToString 規則
// var b = a.toString(); 同上
var c = "3.14";
var d = Number( c ); // Number(..) 遵循前面講過的 ToNumber 規則
// var d = +c; 同上 +是運算子的一元(unary)形式
b; // "42"
d; // 3.14
var c = "3.14";
var d = 5+ +c;
d; // 8.14
一元運算子 - 和 + 一樣,并且它還會反轉數字的符號位,由于 -- 會被當作遞減運算子來處理,所以我們不能使用 -- 來撤銷反轉,而應該像 - -"3.14" 這樣,在中間加一個空格,才能得到正確結果 3.14,
1 + - + + + - + 1; // 2 負負得正
+new Date();// 日期顯式轉換為數字
Date.now();// 比較好的寫法
// Date.now的polyfill 不建議對日期型別使用強制型別轉換,應該使用 Date.now() 來獲得當前的時間戳,使用 new Date(..).getTime() 來獲得指定時間的時間戳,
if (!Date.now) {
Date.now = function() {
return +new Date();
};
}
JavaScript 有一處奇特的語法,即建構式沒有引數時可以不用帶 (),于是我們可能會碰到 var timestamp = +new Date; 這樣的寫法,這樣能否提高代碼可讀性還存在爭議,因為這僅用于 new fn(),對一般的函式呼叫 fn() 并不適用,
奇特的 ~ 運算子
~x 大致等同于 -(x+1),
用法
if (!~a.indexOf( "ol" )) { // true 這里~ 比 >= 0 和 == -1 更簡潔,
// 沒有找到匹配!
}
~12.1 // -13
-(12.1+1) // -13.1
~~12.1 // 12
字位截除
~~x 能將值截除為一個 32 位整數,x | 0 也可以,而且看起來還更簡潔,
Math.floor( -49.6 ); // -50
~~-49.6; // -49
~~1E20 / 10; // 166199296
1E20 | 0 / 10; // 1661992960
(1E20 | 0) / 10; // 166199296
顯式決議數字字串
決議允許字串中含有非數字字符,決議按從左到右的順序,如果遇到非數字字符就停止,而轉換不允許出現非數字字符,否則會失敗并回傳 NaN,決議字串中的浮點數可以使用 parseFloat(..) 函式;
var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42
ES5 之前的 parseInt(..) 有一個坑導致了很多 bug,即如果沒有第二個引數來指定轉換的基數(又稱為 radix),parseInt(..) 會根據字串的第一個字符來自行決定基數,從 ES5 開始 parseInt(..) 默認轉換為十進制數,除非另外指定,如果你的代碼需要在 ES5 之前的環境運行,請記得將第二個引數設定為 10,
決議非字串
parseInt( 1/0, 19 ); // 18
parseInt( 0.000008 ); // 0 ("0" 來自于 "0.000008")
parseInt( 0.0000008 ); // 8 ("8" 來自于 "8e-7")
parseInt( false, 16 ); // 250 ("fa" 來自于 "false")
parseInt( parseInt, 16 ); // 15 ("f" 來自于 "function..")
parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2 3在二進制中不存在,所以取10 轉10進制 為2
parseInt(1/0, 19) 實際上是 parseInt("Infinity", 19),第一個字符是 "I",以 19 為基數時值為 18,第二個字符 "n" 不是一個有效的數字字符,決議到此為止,和 "42px" 中的 "p"一樣
顯式轉換為布林值: 使用 Boolean(a) 和 !!a 來進行顯式強制型別轉換
隱式強制型別轉換: 代碼可讀性不好,但是也是可以減少冗余,讓代碼更簡潔 抽象和隱藏那些細枝末節,有助于提高代碼的可讀性
字串和數字之間的隱式強制型別轉換
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4" 陣列的valueOf() 操作無法得到簡單基本型別值,于是它轉而呼叫 toString()
a + "" 會對 a 呼叫 valueOf() 方法,然后通過 ToString 抽象操作將回傳值轉換為字串,
var a = {
valueOf: function() { return 42; },
toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"
布林值到數字的隱式強制型別轉換
如果其中有且僅有一個引數為 true,則 onlyOne(..) 回傳 true,
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
// 跳過假值,和處理0一樣,但是避免了NaN
if (arguments[i]) {
sum += arguments[i];
}
}
return sum == 1;
}
var a = true;
var b = false;
onlyOne( b, a ); // true
onlyOne( b, a, b, b, b ); // true
|| 和 &&:選擇器運算子”(selector operators)或者“運算元選擇器運算子”(operand selector operators)
在 a ? a : b 中,如果 a 是一個復雜一些的運算式(比如有副作用的函式呼叫等),它有可能被執行兩次(如果第一次結果為真),而在 a || b 中 a 只執行一次,其結果用于條件判斷和回傳結果(如果適用的話),
Symbol 符號的強制型別轉換
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError
== 允許在相等比較中進行強制型別轉換,而 === 不允許
== 和 === 都會檢查運算元的型別,區別在于運算元型別不同時它們的處理方式不同,
抽象相等:==
字串和數字之間的相等比較:
(1) 如果 Type(x) 是數字,Type(y) 是字串,則回傳 x == ToNumber(y) 的結果,
(2) 如果 Type(x) 是字串,Type(y) 是數字,則回傳 ToNumber(x) == y 的結果,
其他型別和布爾型別之間的相等比較:
(1) 如果 Type(x) 是布爾型別,則回傳 ToNumber(x) == y 的結果;
(2) 如果 Type(y) 是布爾型別,則回傳 x == ToNumber(y) 的結果,
null 和 undefined 之間的相等比較:
(1) 如果 x 為 null,y 為 undefined,則結果為 true,
(2) 如果 x 為 undefined,y 為 null,則結果為 true,
var a = 42;
var b = "42";
a === b; // false
a == b; // true
var x = true;
var y = "42";
x == y; // false
var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
a == null 等價為 a=null&& a=undefined
物件和非物件之間的相等比較:
(1) 如果 Type(x) 是字串或數字,Type(y) 是物件,則回傳 x == ToPrimitive(y) 的結果;
(2) 如果 Type(x) 是物件,Type(y) 是字串或數字,則回傳 ToPromitive(x) == y 的結果,
var a = "abc";
var b = Object( a ); // 和new String( a )一樣 其他型別和這個相似,比如number symbol boolean
a === b; // false
a == b; // true
var a = null;
var b = Object( a ); // 和Object()一樣
a == b; // false
var c = undefined;
var d = Object( c ); // 和Object()一樣
c == d; // false
var e = NaN;
var f = Object( e ); // 和new Number( e )一樣
e == f; // false
因為沒有對應的封裝物件,所以 null 和 undefined 不能夠被封裝(boxed),Object(null)和 Object() 均回傳一個常規物件,NaN 能夠被封裝為數字封裝物件,但拆封之后 NaN == NaN 回傳 false,因為 NaN 不等于 NaN
"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- 暈!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 暈!
false == ""; // true -- 暈!
false == []; // true -- 暈!
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- 暈!
"" == []; // true -- 暈!
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 暈!
0 == {}; // false
[] == ![] // true
2 == [2]; // true
"" == [null]; // true
0 == "\n"; // true
"0" == false; // true -- 暈!
false == 0; // true -- 暈!
false == ""; // true -- 暈!
false == []; // true -- 暈!
"" == 0; // true -- 暈!
"" == []; // true -- 暈!
0 == []; // true -- 暈!
安全運用隱式強制型別轉換
如果兩邊的值中有 true 或者 false,千萬不要使用 ==,
如果兩邊的值中有 []、"" 或者 0,盡量不要使用 ==,
抽象關系比較
比較雙方首先呼叫 ToPrimitive,如果結果出現非字串,就根據 ToNumber 規則將雙方強制型別轉換為數字來進行比較,
var a = [ 42 ];
var b = [ "43" ];
a < b; // true
b < a; // false
var a = [ "42" ];
var b = [ "043" ];
a < b; // false '42' < '043'
var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];
a < b; // false "4, 2" < "0, 4, 3"
var a = { b: 42 };
var b = { b: 43 };
a < b; // false 都是[object Object]
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true // 根據規范 a <= b 被處理為 b < a,然后將結果反轉,因為 b < a 的結果是 false,所以 a <= b 的結果是 true,
JavaScript 中 <= 是“不大于”的意思(即 !(a > b),處理為 !(b < a)),同理 a >= b 處理為 b <= a,
相等比較有嚴格相等,關系比較卻沒有“嚴格關系比較”(strict relational comparison),也就是說如果要避免 a < b 中發生隱式強制型別轉換,我們只能確保 a 和 b 為相同的型別,除此之外別無他法,
比較的時候,最好保證一下左右的型別一致;
第五章 語法
5.1 語法和運算式
代碼塊的結果值就如同一個隱式的回傳,即回傳最后一個陳述句的結果值,
var a, b;
a = if (true) { // Uncaught SyntaxError: Unexpected token 'if'
b = 4 + 38;
};
var a, b;
a = eval( "if (true) { b = 4 + 38; }" ); // 但是這樣可以
a; // 42
var a = 42, b;
b = ( a++, a );
a; // 43
b; // 43
++a++ 會產生 ReferenceError 錯誤,因為運算子需要將產生的副作用賦值給一個變數,以 ++a++ 為例,它首先執行 a++(根據運算子優先級,如下),回傳 42,然后執行 ++42,這時會產生 ReferenceError 錯誤,因為 ++ 無法直接在 42 這樣的值上產生副作用,
var obj = {
a: 42
};
obj.a; // 42
delete obj.a; // true 操作成功是指對于那些不存在或者存在且可配置 的屬性,delete 回傳 true,否則回傳 false 或者報錯,
obj.a; // undefined
= 賦值運算子: a =2 這里把 2 賦值給了 a,并且會回傳這個結果,所以可以使用鏈式賦值;
[] + {}; // "[object Object]" [] + {} 會被當作一個值(空物件)來處理
{} + []; // 0 {}會當成一個獨立的空代碼塊(不執行任何操作),所以結果是 + [] 為 0
JavaScript 沒有 else if,但 if 和 else 只包含單條陳述句的時候可以省略代碼塊的{ },
5.2 運算子優先級
,的優先級是最低的;
&& 運算子的優先級高于 =;
&& 運算子先于 || 執行;
|| 的優先級又高于 ? :;
短路:對 && 和 || 來說,如果從左邊的運算元能夠得出結果,就可以忽略右邊的運算元,我們將這種現象稱為“短路”(即執行最短路徑),
&& 運算子是左關聯(|| 也是)
? : 是右關聯
= 是右關聯
var a = foo() && bar(); // foo() 首先執行,它的回傳結果決定了 bar() 是否執行 左關聯
true ? false : true ? true : true; // false
true ? false : (true ? true : true); // false 右關聯
(true ? false : true) ? true : true; // true
var a = 42;
var b = "foo";
var c = false;
var d = a && b || c ? c || b ? a : c && b : a;
d; // 42 (a && b || c) ? (c || (b ? a : c && b)) : a; 這個順序
自動分號: 有時 JavaScript 會自動為代碼行補上缺失的分號,即自動分號插入(Automatic Semicolon Insertion,ASI),只有在代碼行末尾與換行符之間除了空格和注釋之外沒有別的內容時,它才會這樣做,
語法規定 do..while 回圈后面必須帶 ;,而 while 和 for 回圈后則不需要,大多數開發人員都不記得這一點,此時 ASI 就會自動補上分號,
其他涉及 ASI 的情況是 break、continue、return 和 yield(ES6)等關鍵字,如果換行了也沒有分號,會自動補全分號;
5.4 錯誤
JavaScript 中有很多錯誤型別,分為兩大類:早期錯誤(編譯時錯誤,無法被捕獲)和運行時錯誤(可以通過 try..catch 來捕獲),所有語法錯誤都是早期錯誤,程式有語法錯誤則無法運行,
TDZ(Temporal Dead Zone,暫時性死區):由于代碼中的變數還沒有初始化而不能被參考的情況,不能提升變數;
對未宣告變數使用 typeof 不會產生錯誤(參見第 1 章),但在 TDZ 中卻會報錯;
5.5 函式引數
var b = 3;
function foo( a = 42, b = a + b + 5 ) {
// ..
}
foo()// Uncaught ReferenceError: Cannot access 'b' before initialization
b = a + b + 5 在引數 b(= 右邊的 b,而不是函式外的那個)的 TDZ 中訪問 b,所以會出錯,而訪問 a 卻沒有問題,因為此時剛好跨出了引數 a 的 TDZ,
function foo( a = 42, b = a + 1 ) {
console.log( a, b );
}
foo(); // 42 43
foo( undefined ); // 42 43
foo( 5 ); // 5 6
foo( void 0, 7 ); // 42 7
foo( null ); // null 1
ES6 引數默認值會導致 arguments 陣列和相對應的命名引數之間出現偏差,ES5 也會出現這種情況:向函式傳遞引數時,arguments 陣列中的對應單元會和命名引數建立關聯(linkage)以得到相同的值,相反,不傳遞引數就不會建立關聯,在嚴格模式中并沒有建立關聯
function foo(a) {
a = 42;
console.log( arguments[0] );
}
foo( 2 ); // 42 (linked)
foo(); // undefined (not linked)
5.6 try..finally
先執行 finally,再執行 try 或 catch;
如果 finally 中拋出例外,函式就會在此處終止,如果此前 try 中已經有 return 設定了回傳值,則該值會被丟棄:
function foo() {
try {
return 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// 42
function foo() {
try {
throw 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// Uncaught Exception: 42
function foo() {
try {
return 42;
}
finally {
throw "Oops!";
}
console.log( "never runs" );
}
console.log( foo() );
// Uncaught Exception: Oops!
5.7 switch
case 運算式的匹配演算法與 ===相同
var a = "42";
switch (true) {
case a == 10:
console.log( "10 or '10'" );
break;
case a == 42:
console.log( "42 or '42'" );
break;
default:
// 永遠執行不到這里
}
// 42 or '42'
第二部分 異步和性能
第 1 章 異步:現在與將來
事件回圈:執行緒提供了一種機制來處理程式中多個塊的執行,且執行每塊時呼叫 JavaScript 引擎;
setTimeout(..) 并沒有把回呼函式掛在事件回圈佇列中,它所做的是設定一個定時器,當定時器到時后,環境會把你的回呼函式放在事件回圈中,這樣,在未來某個時刻的 tick 會摘下并執行這個回呼,如果事件回圈中已經有很多個函式,則會按順序執行,所以定時器的精度不高,只能保證不會在設定的時間之前執行
異步是關于現在和將來的時間間隙,而并行是關于能夠同時發生的事情;
并行計算最常見的工具就是行程和執行緒,行程和執行緒獨立運行,并可能同時運行:在不同的處理器,甚至不同的計算機上,但多個執行緒能夠共享單個行程的記憶體,
異步是單執行緒執行,只是不確定代碼的執行順序,具有完整運行特性,但是有順序執行的,并行是多執行緒同時執行,共享記憶體,不具有完整運行特性;
完整運行(run-to-completion)特性:由于 JavaScript 的單執行緒特性,foo()(以及 bar())中的代碼具有原子性,也就是說如果 foo() 開始運行,它的所有代碼都會在 bar() 中的任意代碼運行之前完成;
競態條件:函式順序的不確定性,無法可靠預測最終結果;
1.4 并發
單執行緒事件回圈是并發的一種形式
如果行程間沒有相互影響的話,不確定性是完全可以接受的,
并發協作:取到一個長期運行的“行程”,并將其分割成多個步驟或多批任務,使得其他并發“行程”有機會將自己的運算插入到事件回圈佇列中交替運行,js 可以通過 setTimeout 將分成其他小段的任務重新進行異步調度,把剩下的插入到當前時間回圈佇列的結尾處;
var res = [];
// response(..)從Ajax呼叫中取得結果陣列
function response(data) {
// 一次處理1000個
var chunk = data.splice( 0, 1000 );
// 添加到已有的res組
res = res.concat(
// 創建一個新的陣列把chunk中所有值加倍
chunk.map( function(val){
return val * 2;
} )
);
// 還有剩下的需要處理嗎?
if (data.length > 0) {
// 異步調度下一次批處理
setTimeout( function(){
response( data );
}, 0 );
}
}
// ajax(..)是某個庫中提供的某個Ajax函式
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
嚴格說來,setTimeout(..0) 并不直接把專案插入到事件回圈佇列,定時器會在有機會的時候插入事件,舉例來說,兩個連續的 setTimeout(..0) 呼叫不能保證會嚴格按照呼叫順序處理,所以各種情況都有可能出現;
代碼中陳述句的順序和 JavaScript 引擎執行陳述句的順序并不一定要一致;
一旦有事件需要運行,事件回圈就會運行,直到佇列清空,事件回圈的每一輪稱為一個 tick,用戶互動、IO 和定時器會向事件佇列中加入事件,
并發是指兩個或多個事件鏈隨時間發展交替執行,以至于從更高的層次來看,就像是同時在運行(盡管在任意時刻只處理一個事件),
通常需要對這些并發執行的“行程”(有別于作業系統中的行程概念)進行某種形式的互動協調,比如需要確保執行順序或者需要防止競態出現,這些“行程”也可以通過把自身分割為更小的塊,以便其他“行程”插入進來,
第 2 章 回呼
程式的延續(continuation),
一旦我們以回呼函式的形式引入了單個 continuation(或者幾十個),我們就容許了大腦作業方式和代碼執行方式的分歧,一旦這兩者出現分歧,我們就得面對這樣一個無法逆轉的事實:代碼變得更加難以理解、追蹤、除錯和維護,
第一,大腦對于事情的計劃方式是線性的、阻塞的、單執行緒的語意,但是回呼表達異步流程的方式是非線性的、非順序的,這使得正確推導這樣的代碼難度很大,難于理解的代碼是壞代碼,會導致壞 bug,
第二,也是更重要的一點,回呼會受到控制反轉的影響,因為回呼暗中把控制權交給第三方(通常是不受你控制的第三方工具!)來呼叫你代碼中的 continuation,這種控制轉移導致一系列麻煩的信任問題,比如回呼被呼叫的次數是否會超出預期,
第 3 章 Promise
3.1 什么是 promise
- 現在值與將來值,實作 x + y,x 和 y 都是異步獲取的;
- promise;
// 回呼方法
function add(getX,getY,cb) {
var x, y;
getX( function(xVal){
x = xVal;
// 兩個都準備好了?
if (y != undefined) {
cb( x + y ); // 發送和
}
} );
getY( function(yVal){
y = yVal;
// 兩個都準備好了?
if (x != undefined) {
cb( x + y ); // 發送和
}
} );
}
// fetchX() 和fetchY()是同步或者異步函式
add( fetchX, fetchY, function(sum){
console.log( sum ); // 是不是很容易?
} );
// promise實作
function add(xPromise,yPromise) {
// Promise.all([ .. ])接受一個promise陣列并回傳一個新的promise,
// 這個新promise等待陣列中的所有promise完成
return Promise.all( [xPromise, yPromise] )
// 這個promise決議之后,我們取得收到的X和Y值并加在一起
.then( function(values){
// values是來自于之前決議的promisei的訊息陣列
return values[0] + values[1];
} );
}
// fetchX()和fetchY()回傳相應值的promise,可能已經就緒,
// 也可能以后就緒
add( fetchX(), fetchY() )
.then(
// 完成處理函式
function(sum) {
console.log( sum );
},
// 拒絕處理函式
function(err) {
console.error( err ); // 煩!
}
);
Promise 決議后就是外部不可變的值,我們可以安全地把這個值傳遞給第三方,并確信它不會被有意無意地修改,特別是對于多方查看同一個 Promise 決議的情況,尤其如此,一方不可能影響另一方對 Promise 決議的觀察結果,
Promise 是一種封裝和組合未來值的易于復用的機制,
一旦 Promise 決議,它就永遠保持在這個狀態,此時它就成為了不變值(immutable value),可以根據需求多次查看,
3.2 具有 then 方法的鴨子型別
鴨子型別(duck typing):“如果它看起來像只鴨子,叫起來像只鴨子,那它一定就是只鴨子“
thenable 物件的判斷如下:
if (
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
) {
// 假定這是一個thenable! 這種方法可能會導致我正好有個then方法,被判斷為thenable物件了
}
else {
// 不是thenable
}
3.3 Promise 信任問題
異步編碼的可信任問題:
- 呼叫回呼過早;一個 Promise 呼叫 then(..) 的時候,即使這個 Promise 已經決議,提供給 then(..) 的回呼也總會被異步呼叫,所以 promise 不會有這種問題;
- 呼叫回呼過晚;一個 Promise 決議后,這個 Promise 上所有的通過 then(..) 注冊的回呼都會在下一個異步時機點上依次被立即呼叫;
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// A B C
- 回呼未呼叫:沒有任何東西(甚至 JavaScript 錯誤)能阻止 Promise 向你通知它的決議(如果它決議了的話),如果你對一個 Promise 注冊了一個完成回呼和一個拒絕回呼,那么 Promise 在決議時總是會呼叫其中的一個;
// 如果 Promise 本身永遠不被決議呢?即使這樣,Promise 也提供了解決方案,其使用了一種稱為競態的高級抽象機制:
// 用于超時一個Promise的工具
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}
// 設定foo()超時
Promise.race( [
foo(), // 試著開始foo()
timeoutPromise( 3000 ) // 給它3秒鐘
] )
.then(
function(){
// foo(..)及時完成!
},function(err){
// 或者foo()被拒絕,或者只是沒能按時完成
// 查看err來了解是哪種情況
}
);
- 呼叫回呼次數過少或過多;Promise 只能被決議一次,所以任何通過 then(..) 注冊,被呼叫的次數就會和注冊次數相同,
- 未能傳遞所需的環境和引數:Promise 至多只能有一個決議值(完成或拒絕),后面的決議全部失效,包括引數;
- 吞掉可能出現的錯誤和例外:在決議之前有任何的錯誤或例外,會直接走到拒絕中去,但是如果在完成里面有例外,會直接拋出例外,
- Promise.resolve 可以規范化一個類 thenable,回傳一個真正的 Promise,如果是 promise,那就是回傳其本身;
// 不要只是這么做:
foo( 42 )
.then( function(v){
console.log( v );
} );
// 而要這么做:
Promise.resolve( foo( 42 ) )
.then( function(v){
console.log( v );
} );
3.4 鏈式流
Promise 的 then 會回傳一個新的 promise 物件
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 創建一個promise并回傳
return new Promise( function(resolve,reject){
// 引入異步!
setTimeout( function(){
// 用值42填充
resolve( v * 2 );
}, 100 );
} );
} )
.then( function(v){
// 在前一步中的100ms延遲之后運行
console.log( v ); // 42
} );
Promise 固有特性:
- 呼叫 Promise 的 then(..) 會自動創建一個新的 Promise 從呼叫回傳,
- 在完成或拒絕處理函式內部,如果回傳一個值或拋出一個例外,新回傳的(可鏈接的)Promise 就相應地決議,
- 如果完成或拒絕處理函式回傳一個 Promise,它將會被展開,這樣一來,不管它的決議值是什么,都會成為當前 then(..) 回傳的鏈接 Promise 的決議值,
3.5 錯誤處理
try..catch 不能處理異步代碼模塊;
避免丟失被忽略和拋棄的 Promise 錯誤:
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 數字沒有string函式,所以會拋出錯誤
console.log( msg.toLowerCase() );
}
)
.catch( handleErrors );
3.6 Promise 模式
3.6.1 Promise.all([...])
要等待兩個或更多并行 / 并發的任務都完成才能繼續;
Promise.all([ .. ]) 回傳的主 promise 在且僅在所有的成員 promise 都完成后才會完成,如果這些 promise 中有任何一個被拒絕的話,主 Promise.all([ .. ])promise 就會立即被拒絕,并丟棄來自其他所有 promise 的全部結果,
永遠要記住為每個 promise 關聯一個拒絕 / 錯誤處理函式,特別是從 Promise.all([ .. ])回傳的那一個,
// request(..)是一個Promise-aware Ajax工具
// 就像我們在本章前面定義的一樣
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] )
.then( function(msgs){
// 這里,p1和p2完成并把它們的訊息傳入
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} )
.then( function(msg){
console.log( msg );
} );
3.6.2 Promise.race([...])
與 Promise.all([ .. ]) 類似,一旦有任何一個 Promise 決議為完成,Promise.race([ .. ])就會完成;一旦有任何一個 Promise 決議為拒絕,它就會拒絕,
一項競賽需要至少一個“參賽者”,所以,如果你傳入了一個空陣列,主 race([..]) Promise 永遠不會決議,而不是立即決議,
// request(..)是一個支持Promise的Ajax工具
// 就像我們在本章前面定義的一樣
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
.then( function(msg){
// p1或者p2將贏得這場競賽
return request(
"http://some.url.3/?v=" + msg
);
} )
.then( function(msg){
console.log( msg );
} );
這個可以處理之前說過的超時競賽問題:
// foo()是一個支持Promise的函式
// 前面定義的timeoutPromise(..)回傳一個promise,
// 這個promise會在指定延時之后拒絕
// 為foo()設定超時
Promise.race( [
foo(), // 啟動foo()
timeoutPromise( 3000 ) // 給它3秒鐘
] )
.then(
function(){
// foo(..)按時完成!
},
function(err){
// 要么foo()被拒絕,要么只是沒能夠按時完成,
// 因此要查看err了解具體原因
}
);
3.6.3 all([...])和 race([...]的變體
- none([ .. ]):這個模式類似于 all([ .. ]),不過完成和拒絕的情況互換了,所有的 Promise 都要被拒絕,即拒絕轉化為完成值,反之亦然,
- any([ .. ]):這個模式與 all([ .. ]) 類似,但是會忽略拒絕,所以只需要完成一個而不是全部,
- first([ .. ]):這個模式類似于與 any([ .. ]) 的競爭,即只要第一個 Promise 完成,它就會忽略后續的任何拒絕和完成,
- last([ .. ]):這個模式類似于 first([ .. ]),但卻是只有最后一個完成勝出,
// polyfill安全的guard檢查
if (!Promise.first) {
Promise.first = function(prs) {
return new Promise( function(resolve,reject){
// 在所有promise上回圈
prs.forEach( function(pr){
// 把值規整化
Promise.resolve( pr )
// 不管哪個最先完成,就決議主promise
.then( resolve );
} );
} );
};
}
3.6.4 并發迭代
在一列 Promise 中迭代,并對所有 Promise 都執行某個任務;
var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Oops" );
// 把串列中的值加倍,即使是在Promise中
Promise.map( [p1,p2,p3], function(pr,done){
// 保證這一條本身是一個Promise
Promise.resolve( pr )
.then(
// 提取值作為v
function(v){
// map完成的v到新值
done( v * 2 );
},
// 或者map到promise拒絕訊息
done
);
if (!Promise.map) {
Promise.map = function(vals,cb) {
// 一個等待所有map的promise的新promise
return Promise.all(
// 注:一般陣列map(..)把值陣列轉換為 promise陣列
vals.map( function(val){
// 用val異步map之后決議的新promise替換val
return new Promise( function(resolve){
cb( val, resolve );
} );
} )
);
};
}
3.7 Promise API 概述
- new Promise 構造器
- Promise.resolve(..) 和 Promise.reject(..)
- then(..) 和 catch(..)
- Promise.all([ .. ]) 和 Promise.race([ .. ])
3.8 Promise 局限性
- 順序錯誤處理,Promise 鏈中的錯誤很容易被無意中默默忽略掉
- 單一值 ,可以用解構形式優化
- 單決議:只能決議一次
- 慣性:將回呼的方式改為 then,把需要回呼的函式封裝為支持 Promise 的函式這個動作有時被稱為“提升”或“Promise 工廠化”,
- 無法取消的 Promise:一旦創建了一個 Promise 并為其注冊了完成和 / 或拒絕處理函式,如果出現某種情況使得這個任務懸而未決的話,你也沒有辦法從外部停止它的行程,
- Promise 性能:認 Promise 通常要比其非 Promise、非可信任回呼的等價系統稍微慢一點
第 4 章 生成器
4.1 打破完整運行
生成器就是一類特殊的函式,可以一次或多次啟動和停止,并不一定非得要完成,
- 迭代訊息傳遞
function* foo(x) {
var y = x * (yield);
return y;
}
var it = foo(6);
// 啟動foo(..)
it.next();
var res = it.next(7);
res.value; // 42
// 首先,傳入 6 作為引數 x,然后呼叫 it.next(),這會啟動 *foo(..), 在 *foo(..) 內部,開始執行陳述句 var y = x ..,但隨后就遇到了一個 yield 運算式,它
//就會在這一點上暫停 *foo(..)(在賦值陳述句中間!),并在本質上要求呼叫代碼為 yield
//運算式提供一個結果值,接下來,呼叫 it.next( 7 ),這一句把值 7 傳回作為被暫停的
//yield 運算式的結果,
// 第一個 next(..) 總是啟動一個生成器,并運行到第一個 yield 處,不過,是第二個
//next(..) 呼叫完成第一個被暫停的 yield 運算式,第三個 next(..) 呼叫完成第二個 yield,
//以此類推,
- 雙向訊息傳遞系統
function* foo(x) {
var y = x * (yield "Hello"); // <-- yield一個值!
return y;
}
var it = foo(6);
var res = it.next(); // 第一個next(),并不傳入任何東西
res.value; // "Hello"
res = it.next(7); // 向等待的yield傳入7
res.value; // 42
// 在生成器的起始處我們呼叫第一個 next() 時,還沒有暫停的 yield 來接受這樣一個值
// 啟動生成器時一定要用不帶引數的 next()
如果生成器中沒有 return 來停止迭代,那么會有個隱式的 return undefined;
同一個生成器可以同時生產多個迭代器,而這些迭代器可以交替執行;
4.2 生成器產生值
手寫迭代器
var something = (function () {
var nextVal;
return {
// for..of回圈需要
[Symbol.iterator]: function () { return this; }, // 實作這個就可以通過for of迭代
// 標準迭代器介面方法
next: function () {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
return { done: false, value: nextVal };
}
};
})();
iterable(可迭代):一個包含可以在其值上迭代的迭代器的物件,
從一個 iterable 中提取迭代器的方法是:iterable 必須支持一個函式,其名稱是專門的 ES6 符號值 Symbol.iterator,呼叫這個函式時,它會回傳一個迭代器,
也可以手工呼叫這個函式,然后使用它回傳的迭代器:
var a = [1,3,5,7,9];
var it = a[Symbol.iterator]( "Symbol.iterator");
it.next().value; // 1
it.next().value; // 3
it.next().value; // 5
生成器會在每次迭代中暫停,通過 yield 回傳到主程式或事件回圈佇列中
for..of 回圈內的 break 會觸發 finally 陳述句 ,也可以在外部通過 return(..) 手工終止生成器的迭代器實體
function *something() {
try {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
// 清理子句
finally {
console.log( "cleaning up!" );
}
}
var it = something();
for (var v of it) {
console.log( v );
// 不要死回圈!
if (v > 500) {
console.log(
// 完成生成器的迭代器
it.return( "Hello World" ).value
);
// 這里不需要break
}
}
// 1 9 33 105 321 969
// 清理!
// Hello World
4.3 異步迭代生成器
實作順序同步:
function foo(x,y) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
function(err,data){
if (err) {
// 向*main()拋出一個錯誤
it.throw( err );
}
else {
// 用收到的data恢復*main()
it.next( data );
}
}
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
var it = main();
// 這里啟動!
it.next();
同樣可以實作同步錯誤處理
function* main() {
var x = yield "Hello World";
yield x.toLowerCase(); // 引發一個例外!
}
var it = main();
it.next().value; // Hello World
try {
it.next(42);
}
catch (err) {
console.error(err); // TypeError
}
4.4 生成器+Promise
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
var it = main();
var p = it.next().value;
// 等待promise p決議
p.then(
function(text){
it.next( text );
},
function(err){
it.throw( err );
}
);
支持 Promise 的 Generator Runner:
// 在此感謝Benjamin Gruenbaum (@benjamingr on GitHub)的巨大改進!
function run(gen) {
var args = [].slice.call(arguments, 1), it;
// 在當前背景關系中初始化生成器
it = gen.apply(this, args);
// 回傳一個promise用于生成器完成
return Promise.resolve()
.then(function handleNext(value) {
// 對下一個yield出的值運行
var next = it.next(value);
return (function handleResult(next) {
// 生成器運行完畢了嗎?
if (next.done) {
return next.value;
}
// 否則繼續運行
else {
return Promise.resolve(next.value)
.then(
// 成功就恢復異步回圈,把決議的值發回生成器
handleNext,
// 如果value是被拒絕的 promise,
// 就把錯誤傳回生成器進行出錯處理
function handleErr(err) {
return Promise.resolve(
it.throw(err)
)
.then(handleResult);
}
);
}
})(next);
});
}
上面的 run 雖然可以執行同步的 promise,但是不能讓多個異步并發執行;
Promise 所有的并發能力在生成器 +Promise 方法中都可以使用
function* foo() {
// 讓兩個請求"并行",并等待兩個promise都決議
var results = yield Promise.all([
request("http://some.url.1"),
request("http://some.url.2")
]);
var r1 = results[0];
var r2 = results[1];
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log(r3);
}
// 使用前面定義的工具run(..)
run(foo);
4.5 生成器委托
yield * 把迭代器實體控制(當前 *bar() 生成器的)委托給 / 轉移到了這另一個 *foo() 迭代器;
function* foo () {
console.log('*foo() starting');
yield 3;
yield 4;
console.log('*foo() finished');
}
function* bar () {
yield 1;
yield 2;
yield* foo(); // yield委托!
yield 5;
}
var it = bar();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // *foo() starting
// 3
console.log(it.next().value); // 4
console.log(it.next().value); // *foo() finished
// 5
yield * 暫停了迭代控制,而不是生成器控制,當你呼叫 *foo() 生成器時,現在 yield 委托到了它的迭代器,但實際上,你可以 yield 委托到任意 iterable,yield *[1,2,3] 會消耗陣列值 [1,2,3] 的默認迭代器,
yield 委托的主要目的是代碼組織,以達到與普通函式呼叫的對稱,
function* foo() {
console.log("inside *foo():", yield "B");
console.log("inside *foo():", yield "C");
return "D";
}
function* bar() {
console.log("inside *bar():", yield "A");
// yield委托!
console.log("inside *bar():", yield* foo());
console.log("inside *bar():", yield "E");
return "F";
}
var it = bar();
console.log("outside:", it.next().value);
// outside: A
console.log("outside:", it.next(1).value);
// inside *bar(): 1
// outside: B
console.log("outside:", it.next(2).value);
// inside *foo(): 2
// outside: C
console.log("outside:", it.next(3).value);
// inside *foo(): 3
// inside *bar(): D
// outside: E
console.log("outside:", it.next(4).value);
// inside *bar(): 4
// outside: F
(1) 值 3(通過 *bar() 內部的 yield 委托)傳入等待的 *foo() 內部的 yield "C" 運算式,
(2) 然后 *foo() 呼叫 return "D",但是這個值并沒有一直回傳到外部的 it.next(3) 呼叫,
(3) 取而代之的是,值 "D" 作為 bar() 內部等待的 yieldfoo() 運算式的結果發出——這個 yield 委托本質上在所有的 *foo() 完成之前是暫停的,所以 "D" 成為 *bar() 內部的最后結果,并被列印出來,
(4) yield "E" 在 *bar() 內部呼叫,值 "E" 作為 it.next(3) 呼叫的結果被 yield 發出,
yield 委托甚至并不要求必須轉到另一個生成器,它可以轉到一個非生成器的一般 iterable,
function* bar() {
console.log("inside *bar():", yield "A");
// yield委托給非生成器!
console.log("inside *bar():", yield* ["B", "C", "D"]);
console.log("inside *bar():", yield "E");
return "F";
}
var it = bar();
console.log("outside:", it.next().value);
// outside: A
console.log("outside:", it.next(1).value);
// inside *bar(): 1
// outside: B
console.log("outside:", it.next(2).value);
// outside: C
console.log("outside:", it.next(3).value);
// outside: D
console.log("outside:", it.next(4).value);
// inside *bar(): undefined
// outside: E
console.log("outside:", it.next(5).value);
// inside *bar(): 5
// outside: F
例外也可以被委托
function *foo() {
try {
yield "B";
}
catch (err) {
console.log( "error caught inside *foo():", err );
}
yield "C";
throw "D";
}
function *bar() {
yield "A";
try {
yield *foo();
}
catch (err) {
console.log( "error caught inside *bar():", err );
}
yield "E";
yield *baz();
// 注:不會到達這里!
yield "G";
}
function *baz() {
throw "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// outside: B
console.log( "outside:", it.throw( 2 ).value );
// error caught inside *foo(): 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// error caught inside *bar(): D
// outside: E
try {
console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
console.log( "error caught outside:", err );
}
// error caught outside: F
遞回委托
function *foo(val) {
if (val > 1) {
// 生成器遞回
val = yield *foo( val - 1 );
}
return yield Promise.resolve(() => {
console.log('settimeout', val);
});
}
function *bar() {
var r1 = yield *foo( 3 );
// console.log( r1 );
}
4.6 生成器并發
通信順序行程(Communicating Sequential Processes,CSP): 講道理 這里沒看懂 跳過吧
4.7 形式轉換程式
形實轉換程式(thunk):JavaScript 中的 thunk 是指一個用于呼叫另外一個函式的函式,沒有任何引數
function foo(x, y, cb) {
setTimeout(function () {
cb(x + y);
}, 1000);
}
function fooThunk(cb) {
foo(3, 4, cb);
}
// 將來
fooThunk(function (sum) {
console.log(sum); // 7
});
第 5 章 程式性能
5.1 Web Worker
Worker 之間以及它們和主程式之間,不會共享任何作用域或資源,那會把所有多執行緒編程 的噩夢帶到前端領域,而是通過一個基本的事件訊息機制相互聯系,
Web Worker 通常應用于哪些方面呢?
- 處理密集型數學計算
- 大資料集排序
- 資料處理(壓縮、音頻分析、影像處理等)
高流量網路通信
使用 worker 的應用需要在執行緒之間通過事件機制傳遞大量的資訊,可能是雙向的,
在早期的 Worker 中,唯一的選擇就是把所有資料序列化到一個字串值中,除了雙向序 列化導致的速度損失之外,另一個主要的負面因素是資料需要被復制,這意味著兩倍的內 存使用(及其引起的垃圾收集方面的波動),
如果要傳遞一個物件,可以使用結構化克隆演算法,這樣就不用付出 to-string 和 from-string 的性能損失了,但是這種方案還是要使用雙倍的記憶體,IE10 及更高版本以及所有其他主流瀏覽器都支持這種方案,
還有一個更好的選擇,特別是對于大資料集而言,就是使用 Transferable 物件
這時發生的是物件所 有權的轉移,資料本身并沒有移動,一旦你把物件傳遞到一個 Worker 中,在原來的位置 上,它就變為空的或者是不可訪問的,這樣就消除了多執行緒編程作用域共享帶來的混亂, 當然,所有權傳遞是可以雙向進行的,
共享 Worker: 整個站點或 app 的所有頁面實體都可以共享的中心 Worker;
如果有某個埠連接終止而其他埠連接仍然活躍,那么共享 Worker 不會 終止,而對專用 Worker 來說,只要到實體化它的程式的連接終止,它就會 終止,
第 6 章 性能測驗和調優
6.1 性能測驗
用重復運行,然后取平均值去測驗性能誤差會比較大,應該基于統計學去實踐,比如 Benchmark.js 庫;
6.2 環境為王
測驗不真實的代碼只能得出不真實的結論,如果有實際可能的話,你 應該測驗實際的而非無關緊要的代碼,測驗條件與你期望的真實情況越接近越好,只有這 樣得出的結果才有可能接近事實,
6.3 jsPerf.com
如果你想要得到可靠的測驗結論的話,就需要在很多不同的環境(桌面瀏覽 器、移動設備,等等)中測驗匯集測驗結果,
6.4 寫好測驗用例
寫有意義的測驗用例;
6.5 微性能
只考慮微性能是不合理的,可讀性,包括一些瀏覽器不同處理問題也要考慮;
6.6 尾呼叫優化
a
尾呼叫:一個出現在另一個函式“結尾”處的函式呼叫;
呼叫一個新的函式需要額外的一塊預留記憶體來管理呼叫 堆疊,稱為堆疊;
尾呼叫優化(TCO):不需要創建一個新的堆疊幀,而是可以重用已 有的 bar(..) 的堆疊幀,這樣不僅速度更快,也更節省記憶體
TCO 允許一個函式在結尾處呼叫另外一個函式來執行,不需要任何額外資源,這意味著,對遞回演算法來說,引擎不再需要限制堆疊深度,
文章來源:NSGUF,歡迎分享,轉載請保留出處轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/546865.html
標籤:其他
上一篇:股票問題-求收益最大值
下一篇:前端設計模式——代理模式
