【讀書筆記】你不知道的JavaScript(中)

来源:https://www.cnblogs.com/NSGUF/archive/2023/03/14/17213579.html
-Advertisement-
Play Games

第一部分 類型和語法 第一章 類型 JavaScript 有七種內置類型: • 空值(null) • 未定義(undefined) • 布爾值( boolean) • 數字(number) • 字元串(string) • 對象(object) • 符號(symbol,ES6 中新增) typeof ...


第一部分 類型和語法

第一章 類型

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

類數組轉換成數組:

  1. Array.prototype.slice.call(arguments);
  2. 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

  1. 該方法可重新定義;
  2. 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

  1. 現在值與將來值,實現 x + y,x 和 y 都是非同步獲取的;
  2. 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 信任問題

非同步編碼的可信任問題:

  1. 調用回調過早;一個 Promise 調用 then(..) 的時候,即使這個 Promise 已經決議,提供給 then(..) 的回調也總會被非同步調用,所以 promise 不會有這種問題;
  2. 調用回調過晚;一個 Promise 決議後,這個 Promise 上所有的通過 then(..) 註冊的回調都會在下一個非同步時機點上依次被立即調用;
p.then( function(){
 p.then( function(){
 console.log( "C" );
 } );
 console.log( "A" );
} );
p.then( function(){
 console.log( "B" );
} );
// A B C
  1. 回調未調用:沒有任何東西(甚至 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來瞭解是哪種情況
 }
);
  1. 調用回調次數過少或過多;Promise 只能被決議一次,所以任何通過 then(..) 註冊,被調用的次數就會和註冊次數相同。
  2. 未能傳遞所需的環境和參數:Promise 至多只能有一個決議值(完成或拒絕),後面的決議全部失效,包括參數;
  3. 吞掉可能出現的錯誤和異常:在決議之前有任何的錯誤或異常,會直接走到拒絕中去,但是如果在完成裡面有異常,會直接拋出異常。
  4. 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 固有特性:

  1. 調用 Promise 的 then(..) 會自動創建一個新的 Promise 從調用返回。
  2. 在完成或拒絕處理函數內部,如果返回一個值或拋出一個異常,新返回的(可鏈接的)Promise 就相應地決議。
  3. 如果完成或拒絕處理函數返回一個 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([...]的變體

  1. none([ .. ]):這個模式類似於 all([ .. ]),不過完成和拒絕的情況互換了。所有的 Promise 都要被拒絕,即拒絕轉化為完成值,反之亦然。
  2. any([ .. ]):這個模式與 all([ .. ]) 類似,但是會忽略拒絕,所以只需要完成一個而不是全部。
  3. first([ .. ]):這個模式類似於與 any([ .. ]) 的競爭,即只要第一個 Promise 完成,它就會忽略後續的任何拒絕和完成。
  4. 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 概述

  1. new Promise 構造器
  2. Promise.resolve(..) 和 Promise.reject(..)
  3. then(..) 和 catch(..)
  4. Promise.all([ .. ]) 和 Promise.race([ .. ])

3.8 Promise 局限性

  1. 順序錯誤處理,Promise 鏈中的錯誤很容易被無意中默默忽略掉
  2. 單一值 ,可以用解構形式優化
  3. 單決議:只能決議一次
  4. 慣性:將回調的方式改為 then,把需要回調的函數封裝為支持 Promise 的函數這個動作有時被稱為“提升”或“Promise 工廠化”。
  5. 無法取消的 Promise:一旦創建了一個 Promise 併為其註冊了完成和 / 或拒絕處理函數,如果出現某種情況使得這個任務懸而未決的話,你也沒有辦法從外部停止它的進程。
  6. Promise 性能:認 Promise 通常要比其非 Promise、非可信任回調的等價系統稍微慢一點

第 4 章 生成器

4.1 打破完整運行

生成器就是一類特殊的函數,可以一次或多次啟動和停止,並不一定非得要完成。

  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,
//以此類推。
  1. 雙向消息傳遞系統
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 中,唯一的選擇就是把所有數據序列化到一個字元串值中。除了雙向序 列化導致的速度損失之外,另一個主要的負面因素是數據需要被覆制,這意味著兩倍的內 存使用(及其引起的垃圾收集方面的波動)。

如果要傳遞一個對象,可

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 前面我們簡單的瞭解了 vue 初始化時的一些大概的流程,這裡我們詳細的瞭解下具體的內容; 這塊建議搭建可以根據 demo 進行 debugger 來觀察; 內容 這一塊主要圍繞init.ts中的mergeOptions進行剖析。 mergeOptions mergeOptions的方法位於sc ...
  • 其他章節請看: webgl 系列 漸變三角形 本文通過一個漸變三角形的示例逐步分析:varying變數、合併緩衝區、圖形裝配、光柵化、varying 內插 繪製三個點v1 需求:繪製三個相同顏色的點,效果如下: 通過三角形的學習,這個需求非常容易實現。代碼如下: const VSHADER_SOUR ...
  • 情景 關鍵 組件沒有正確引入 函數無限遞歸 解決 如果在網上搜索[Vue warn]: Component is missing template or render function. 或[Vue warn]: Invalid vnode type when creating vnode: nul ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 老實說我不喜歡用forEach,因為它導致的一些bug總是這麼不經意,盤點我不喜歡的原因 原因一:不支持處理非同步函數 先看一個例子: async function test() { let arr = [3, 2, 1] arr.forE ...
  • 寫在前面 模塊化開發是我們日常工作潛移默化中用到的基本技能,發展至今非常地簡潔方便,但開發者們(指我自己)卻很少能清晰透徹地說出它的發展背景, 發展過程以及各個規範之間的區別。故筆者決定一探乾坤,深入淺出學習一下什麼是前端模塊化。 通過本文,筆者希望各位能夠收穫到: 前端模塊化發展的大致歷史背景 � ...
  • 代碼如下: 我這邊用的阿裡OSS直傳的,主要思路是獲取截圖對象,粘貼到指定div位置,獲取文件流,將文件流賦值給clientUpload方法。 使用wx/qq 快捷指令就可以截圖上傳圖片了。 <div class="layui-form-item"> <label class="layui-form ...
  • 山海鯨可視化創造了一種CS和BS熱切換的編輯模式,即CSaaS架構,可以在安裝軟體之後一鍵從軟體的CS狀態切換為一個BS伺服器,讓私有化部署變得十分輕鬆。在視頻中我們先在CS模式下的軟體中進行了編輯,隨後熱切換到BS模式下的web中依然可以在幾乎完全相同的體驗下進行編輯。 ...
  • 代理模式(Proxy Pattern):前端設計模式中的代理模式是一種結構型模式,它允許在不改變原始對象的情況下,通過引入一個代理對象來控制對原始對象的訪問。代理對象充當原始對象的中介,客戶端與代理對象交互,代理對象再將請求轉發給原始對象。 代理模式在前端開發中經常被用來處理一些複雜或者耗時的操作, ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...