Effective JavaScript
讓自己習(xí)慣javaScript
1 - 了解你使用的javaScript版本
ES5引入的'嚴(yán)格模式',在js文件連接使用時可能導(dǎo)致問題.
如:
//file1.js "use strict"; function f(){ //... } //file2.js no strict-mode function f(){ //arguments為方法自帶的,在嚴(yán)格模式下重新定義會報錯 var arguments = []; } //兩個倒過來,非嚴(yán)格模式的在前面,又會導(dǎo)致嚴(yán)格模式文件嚴(yán)格模式無效了.
解決方案:
- 避免使用嚴(yán)格模式的js文件和不使用嚴(yán)格模式的js文件連接使用.
- 通過(function(){...})();的方式包裹自身,在被連接后,都會被獨立的解釋執(zhí)行.缺點是,這種方法會導(dǎo)致文件的內(nèi)定義的全局變量,方法等不會被視作全局的.
2 - 理解javaScript浮點數(shù)
tips:
-
javaScript的數(shù)字都是雙精度浮點數(shù),即double(64位浮點數(shù)).可精確表示-253~253之間的數(shù).
數(shù)字類型 .toString(n);可以把數(shù)字轉(zhuǎn)換成對應(yīng)進制展示 注意數(shù)字后要加一個空格
如 console.log(29 .toString(15)); => "1e"
javaScript的整數(shù)僅僅是雙精度浮點數(shù)的一個子集
-
位運算會將數(shù)字視為32位有符號整數(shù)(會直接舍棄后面的小數(shù)位),計算完后把結(jié)果轉(zhuǎn)換成浮點數(shù)
8.1 | 1 => 1000.0001100110011001100110011001100110011001100110011 | 1 => 1000 | 1 => 1001 => 1001(浮點數(shù))
盡量避免使用浮點數(shù)運算,可以的話轉(zhuǎn)換成整數(shù)計算.如金錢計算都乘以100,以分為單位就不會有小數(shù),就不存在浮點精度誤差了.
3 - 當(dāng)心隱式類型轉(zhuǎn)換
由于javaScript對類型錯誤極其寬容導(dǎo)致.JavaScript會按照多種多樣的自動轉(zhuǎn)換協(xié)議將值強制轉(zhuǎn)換為期望的類型.
-
算數(shù)運算符( + - * / % )會嘗試把兩邊都轉(zhuǎn)換成數(shù)字(+ 既重載了數(shù)字+也重載了字符串加法,具體是數(shù)字相加還是字符串相連取決于參數(shù)類型, 從左到右)
null在算數(shù)運算中會被隱式轉(zhuǎn)換成0
-
未定義的變量會變轉(zhuǎn)換成NaN
-
如果知道是數(shù)字,isNaN可以判斷是否是NaN;但如果不是數(shù)字,isNaN無法區(qū)分.
如: isNaN("foo")=>true ,同理undefined,{},{valueOf:"foo"}都會返回true
tips:可以利用NaN是Js中唯一不等于自身的值這個特性, 用 x !== x判斷是否為真的NaN
-
位運算( ~ & ^ | << >> >>> )會把操作數(shù)轉(zhuǎn)換成數(shù)字,且會轉(zhuǎn)換成32位整數(shù)
-
對象隱式轉(zhuǎn)換toString()方法轉(zhuǎn)換成字符串,valueOf方法轉(zhuǎn)換成數(shù)字.除了 "+"外,其他算數(shù)運算符會調(diào)用valueOf的方法轉(zhuǎn)換成數(shù)字計算;位運算也會調(diào)用valueOf轉(zhuǎn)換成數(shù)字計算;而加法的隱式轉(zhuǎn)換常常會出人意料.所以建議對象有valueOf方法時,同時也要定義toString方法,返回與valueOf值一致的字符串結(jié)果.(當(dāng)然,最好避免使用對象的valueOf方法)(見5)
請留意:
var x = "s"; x == "s" //true x === "s" //true new String("s") == new String("s") //false new String("s") === new String("s") //false x == new String("s") //true x === new String("s") //false -
真值轉(zhuǎn)換.JavaScript只有7個假值: false, 0, -0, "", NaN, null, undefined.其他都為真值.
if, || 和 && 等運算符時,將值轉(zhuǎn)換成真值計算.
-
判斷是否為undefined的兩種方式
- typeof x === "undefined"
- x === undefined
4 - 原始類型優(yōu)于封裝對象
除對象外,JavaScript有5個原始值類型:布爾值boolean,數(shù)字number,字符串string,null和undefined
注意: typeof null會返回object
布爾值,數(shù)字,字符串有對應(yīng)的包裝類型Boolean,Number,String
隱式轉(zhuǎn)換②: 隱式封裝.
舉例: "hello".toUpperCase(); //"HELLO"
會生成一個新的String對象,調(diào)用方法返回結(jié)果,且每次隱式封裝都會產(chǎn)生一個新的對象,
所以對原始值設(shè)置屬性是沒有任何意義的.
所以:
"hello".someProperty = 17; "hello".someProperty; //undefined第一次產(chǎn)生的對象和第二次調(diào)用時并不相同,所以第二次會返回undefined
5 - 避免對混合類型使用 == 運算符
當(dāng)兩個參數(shù)屬于同一類型時,使用 == 和 === 運算符沒有區(qū)別.
| 參數(shù)類型1 | 參數(shù)類型2 | 強制轉(zhuǎn)換 |
|---|---|---|
| null | undefined | 不轉(zhuǎn)換,總返回true |
| null/undefined | 非null/非undefined | 不轉(zhuǎn)換,總返回false |
| 原始類型string,number,boolean | Date對象 | 原始類型轉(zhuǎn)換成數(shù)字,Date對象轉(zhuǎn)換成原始類型(優(yōu)先嘗試toString,再嘗試valueOf方法) |
| 原始類型string,number,boolean | 非Date對象 | 原始類型轉(zhuǎn)換成數(shù)字,非Date對象轉(zhuǎn)換成原始類型(優(yōu)先嘗試valueOf,再嘗試toString方法) |
| 原始類型string,number,boolean | 原始類型string,number,boolean | 原始類型轉(zhuǎn)換成數(shù)字 |
解決方案:
- 盡量使用 === 運算符
- 比較不同類型時,自己定義顯示的強制轉(zhuǎn)換方法,讓比較的行為更清晰.
6 - 了解分號插入的局限
分號插入規(guī)則:
-
分號會在 } 標(biāo)記之前, 一個或多個換行之后, 程序輸入的結(jié)尾處被插入.
(即 你可以省略一行后、一個代碼塊和一段程序結(jié)束處的分號)
function square(x){ var n = +x return n * n } function area(r) { r = +r; return Math.PI * r * r} function add1(x) {return x+1} -
分號僅在隨后的輸入標(biāo)記不能解析時插入.
如:
a = b (f());系統(tǒng)會判斷 a = b(f());可以正常解析,所以此處第一行不能省略分號
5個明確有問題的開頭字符: ( [ + - /
- 解決方案:在出現(xiàn)這五個開頭字符時,顯式地在前面加上分號
javaScript限制產(chǎn)生式(restricted production)不允許兩個字符之間出現(xiàn)換行時,會強制插入分號
包括:
-
return語句
return {};等價于
return ; {} ; throw語句
帶有顯式標(biāo)簽的break或continue語句
-
后置自增或自減運算符
a ++ b等價于
a; ++b; //因為后置++ 就會在a后面強制加上分號
-
分號不會作為分隔符在for循環(huán)空語句的頭部被自動插入,空循環(huán)體的while循環(huán)也需要顯式的分號
for(var i = 0, total = 1 //會報錯,不會自動插入分號 i < n i++){}function infiniteLoop(){ while(true) ; }//沒有這個分號會報錯
7 - 視字符串為16位的代碼單元序列
Unicode為所有文字每個字符都分配了唯一整數(shù),整數(shù)介于0和1114111之間,稱為代碼點.
Unicode允許多個不同二進制編碼的代碼點.目前最流行的有UTF-8,UTF-16,UTF-32.
將代碼點與其編碼元素一對一映射起來,稱為一個代碼單元.
由于歷史原因,JavaScript字符串的元素是一個16位的代碼單元,字符串屬性和方法(如length,charAt,charCodeAt)都是基于代碼單元層級,而不是代碼點層級.所以每當(dāng)字符串包含輔助平面的代碼點時(即需要用兩個代碼單元去表示一個代碼點時),javascript將每個代碼點表示為兩個元素而不是一個(一對UTF-16代理對的代碼點).(正則表達式模式也一樣有這個問題)
當(dāng)出現(xiàn)兩個代碼單元表示一個代碼點的時候,length,charAt,charCodeAt等方法在執(zhí)行時,會出現(xiàn)意想不到的結(jié)果.
而例如sendcodeURI,decodeURI,encodeURIComponent和decodeURIComponent等URI操作函數(shù)的ECMAScript庫正確的處理了代理對,就不會有問題.
tips:
- 使用第三方的庫編寫可識別代碼點的字符串操作.
- 每當(dāng)使用一個含有字符串操作的庫時,都要查閱該庫文檔,看他如何處理代碼點的整個范圍.
變量作用域
8 - 盡量少用全局變量
定義全局變量會污染共享的公共命名空間,可能導(dǎo)致意外的命名沖突;不利于模塊化,因為他會導(dǎo)致程序獨立組件之間不必要的耦合.
tips:
避免聲明全局變量,盡量使用局部變量
避免對全局對象添加屬性
-
使用全局對象來做平臺特性檢測
如:ES5引入的全局JSON對象(用于讀寫JSON格式的數(shù)據(jù)),檢測當(dāng)前環(huán)境是否提供了這個對象
if(!this.JSON){ //如果沒有這個對象,就定義一個. this.JSON = { parse: ..., stringify: ... } }
9 - 始終聲明局部變量
不使用var,let,const聲明變量,該變量會隱式的轉(zhuǎn)變?yōu)槿肿兞?/p>
tips:
- 使用lint工具幫助檢查未綁定的變量
10 - 避免使用with
with的簡單介紹:
//with 擴展一個語句的作用域鏈 ES5的嚴(yán)格模式已禁用with //with 出現(xiàn)的目的是減少變量的長度,減少不必要的指針路徑解析運算 with(expression){//將表達式添加到評估語句時使用的作用域鏈上 statement//任何語句 } //舉例 var a, x, y; var r = 10; with(Math){//Math被作為默認對象,PI,cos,sin前面不需要添加命名空間 a = PI * r * r; x = r * cos(PI); y = r * sin(PI / 2); } //with 使程序查找變量時都會先去指定對象中找,那些不屬于他的屬性查找速度會很慢 //另外,with導(dǎo)致了語義不明的嚴(yán)重問題,如下: function f(x,o){ with(o){ print(x);//方法f獲取的參數(shù)x可能取到值,可能為undefined;能取到時,可能在o上,也可能是函數(shù)的第一個參數(shù)(o中沒有x這個屬性的話),人力無法預(yù)測此處的值 } }
tips:
避免使用with語句.不僅性能差,而且有語義問題
-
使用簡單的變量名代替重復(fù)訪問的對象;想要實現(xiàn)with的初衷,可以將局部變量顯式的綁定到相關(guān)屬性上
如:
function(x,y){ var min = Math.min, round = Math.round, sqrt = Math.sqrt; return min(round(x), sqrt(y)); }
11 - 熟練掌握閉包
JavaScript的函數(shù)值包含了比調(diào)用他們時執(zhí)行所需的代碼需要的信息以外的更多的信息.
他們可以獲取在其封閉作用域的變量
那些在其所涵蓋的作用域內(nèi)跟蹤變量的函數(shù),稱為閉包.
閉包的三個事實:
? //此處的外部指的是閉包函數(shù)的外部
- JavaScript允許你引用當(dāng)前函數(shù)以外定義的變量.=>閉包可以引用定義在其外部作用域的變量
- 即使外部函數(shù)已經(jīng)返回,當(dāng)前函數(shù)仍可以引用外部函數(shù)所定義的變量.=>閉包比創(chuàng)建他們的函數(shù)有更長的生命周期
- 閉包可以更新外部變量的值
warning:閉包存儲的外部變量的引用,而不是值!!!
舉例:
//假設(shè)一個獲取全名的方法 //函數(shù)柯里化 function initLastname(lastname){ return function(firstname){ return `${lastname} ${firstname}`; } } //此處獲取定義了姓的方法 const getNameStartWithSheng = initLastname('sheng'); getNameStartWithSheng('wei');// sheng wei getNameStartWithSheng('jie');// sheng jie //重新定義一個新的方法... const getNameStartWithLi = initLastname('li');
12 - 理解變量聲明提升
tips:
javaScript隱式地提升聲明部分到封閉函數(shù)的頂部,而把賦值留在原地;提升方法聲明到頂部,而不提升方法調(diào)用.
在一個作用域中重復(fù)聲明變量,會被視為單個變量.
考慮手動提升局部變量,從而避免混淆.
舉例:
console.log(a);//undefined 不會出現(xiàn)Uncaught ReferenceError錯誤 // console.log(b);//打開的話會出現(xiàn)Uncaught ReferenceError: b is not defined c();//"c" 不會出現(xiàn)Uncaught ReferenceError錯誤 var a = "a"; function c(){ console.log("c"); }因為上面的內(nèi)容等效于
var a; function c(){ console.log("c"); } console.log(a); //console.log(b); c(); a = "a";
13 - 使用立即調(diào)用的函數(shù)表達式創(chuàng)建局部作用域
function wrapElements(a){
var result = [];
for(var i = 0, n = a.length; i < n; i++){
result[i] = function(){return a[i];};
}
return result;
}
var wrapped = wrapElements([10,20,30,40,50]);
var f = wrapped[0];
console.log(f());//undefined 此處為什么不是10而是undefined?
原因: 閉包是由函數(shù)與對其狀態(tài)即詞法環(huán)境(lexical environment)的引用共同構(gòu)成.所以上面產(chǎn)生的每一個閉包都綁定了同一個詞法環(huán)境! 即每個函數(shù)都為 return a[5],因為索引越界所以返回undefined
解決方法:
-
使用更多的閉包,讓回調(diào)不再共享同一個詞法環(huán)境
function wrapElements(a){ var result = []; for(var i = 0, n = a.length; i < n; i++){ result[i] = returnValue(a[i]);//此處多一層閉包,強制不共享同一個詞法環(huán)境 } return result; } function returnValue(value){ return function(){return value;} } var wrapped = wrapElements([10,20,30,40,50]); var f = wrapped[0]; console.log(f());//10 -
創(chuàng)建一個嵌套函數(shù)并立即調(diào)用它來創(chuàng)建一個局部作用域
warnings:要當(dāng)心函數(shù)中包裹代碼塊導(dǎo)致代碼塊的微妙變化.
- 代碼塊不能包含break; continue語句.是不合法的
- 代碼塊如果引用了this或特別的arguments變量,這種方式會改變他們的含義!
function wrapElements(a){ var result = []; for(var i = 0, n = a.length; i < n; i++){ (function(){ var j = i; result[i] = function(){return a[j];}; })(); //此處也可以是如下形式,將局部變量作為實參傳入 //(function(j){ // result[i] = function(){return a[j];}; //})(i); } return result; } var wrapped = wrapElements([10,20,30,40,50]); var f = wrapped[0]; console.log(f());//10 -
使用ES6 let關(guān)鍵字,綁定塊作用域
function wrapElements(a){ var result = []; //僅僅是此處聲明i時使用let.因為let不會提升變量的聲明.每個閉包都綁定了塊作用域的變量 for(let i = 0, n = a.length; i < n; i++){ result[i] = function(){return a[i];}; } return result; } var wrapped = wrapElements([10,20,30,40,50]); var f = wrapped[0]; console.log(f());//10
14 - 當(dāng)心命名函數(shù)表達式笨拙的作用域
缺陷1:
命名函數(shù)表達式,即 function xxx(){};
常用于調(diào)試.在對Error對象的棧跟蹤功能中,函數(shù)表達式的名稱通常作為其入口使用.但是命名函數(shù)表達式是作用域和兼容性問題臭名昭著的來源.
ES3中,JavaScript引擎被要求將命名函數(shù)表達式的作用域表示為一個對象,這個對象只有單個屬性,將函數(shù)名和函數(shù)自身綁定起來,但是這個對象也繼承了Object.prototype的屬性.因此,就把Object.prototype中的所有屬性引入了作用域.
var constructor = function(){return null;}; var f = function f(){ return constructor(); }; f();//{} (in ES3 environments) //因為命名函數(shù)表達式在其作用域內(nèi)集成了Object的構(gòu)造函數(shù) 這種情況刪除函數(shù)表達式名即可 即 var f = function(){ return constructor(); } //存在:一些不符合標(biāo)準(zhǔn)的js環(huán)境中,即使刪除了函數(shù)表達式名,調(diào)用f()依舊會返回一個空對象.
避免方式:
在任何時候都避免對Object.prototype中添加屬性,以及使用與Object.prototype屬性同名的局部變量.
缺陷2:
對命名函數(shù)表達式的聲明進行提升.
//在一些不符合標(biāo)準(zhǔn)的環(huán)境中
var f = function g(){return 17;};
g();//17
//甚至這兩個對象是兩個不同的對象,導(dǎo)致了不必要的內(nèi)存分配
解決辦法:
創(chuàng)建一個與函數(shù)表達式同名的局部變量并賦值為null;//確保重復(fù)的函數(shù)會被垃圾回收.
var f = function g(){return 17;};
var g = null;
tips:
在Error對象和調(diào)試器中使用命名函數(shù)表達式改進棧跟蹤.
在ES3和有問題的js環(huán)境中謹(jǐn)記函數(shù)表達式作用域會被Object.prototype污染.//ES5中已解決bug
謹(jǐn)記在錯誤百出的js環(huán)境中會提升命名函數(shù)表達式聲明,導(dǎo)致重復(fù)存儲
-
考慮避免使用命名函數(shù)表達式或在發(fā)布前刪除函數(shù)名
=> 如果代碼發(fā)布到正確實現(xiàn)的ES5環(huán)境,就不必擔(dān)心上述問題.
15 - 當(dāng)心局部塊函數(shù)聲明笨拙的作用域
function f(){return "global"};
function test(x){
var result = [];
if(x){
function f(){return "local";};
result.push(f());
}
result.push(f());
return result;
}
test(true);//?
test(false);//?
//在ES5及以前,不存在塊級作用域,test內(nèi)部的f()作用域是整個函數(shù).
//因此,結(jié)果為["local","local"]和"["local"]
//在ES6中,存在塊級作用域.
//如上代碼在執(zhí)行到result.push(f())時,會報f is not a function的錯
//將內(nèi)部修改為 let f = function(){return "local";};則返回["local","global"]和"["global"]
16 - 避免使用eval創(chuàng)建局部變量
如: eval("var y = 'hello'; ");這一句在被調(diào)用后才會執(zhí)行,將y變量加入到作用域.
tips:
避免使用eval函數(shù)創(chuàng)建的變量污染調(diào)用者的作用域.
-
如果eval函數(shù)代碼可能創(chuàng)建全局變量,可以將此調(diào)用封裝到嵌套的函數(shù)中,以防止作用域污染.
var y = "global"; function test(src){ (function(){eval(src);})(); return y; } test("var y = 'local';"); //"global" test("var z = 'local';"); //"global"
(remain learning)17 - 間接調(diào)用eval函數(shù)優(yōu)于直接調(diào)用
eval函數(shù)不僅僅是一個函數(shù),還可以訪問調(diào)用它時的整個作用域.
盡可能使用間接的方式調(diào)用eval函數(shù).
兩種調(diào)用eval的方式:
-
直接調(diào)用
var x = "global"; function test(){ var x = "local"; return eval("x");//direct eval } test(); //"local",此時調(diào)用時具有完全訪問局部作用域的權(quán)限 -
間接調(diào)用
var x = "global"; function test(){ var x = "local"; var f = eval;//綁定eval函數(shù)到另一個變量名 return f("x");//通過變量名調(diào)用函數(shù),會使代碼失去對局部作用域訪問的能力 } test();//"global"(0,eval)(src); //(,)表達式序列運算符總會返回最后一項,所以前面無論是0還是其他字面量都沒關(guān)系,都會返回eval,整個表達式被視為間接調(diào)用eval函數(shù) //直接使用evel(src);左側(cè)被視為一個引用 //(0,eval)(src);左側(cè)被視為一個值
使用函數(shù)
18 - 理解函數(shù)調(diào)用、方法調(diào)用及構(gòu)造函數(shù)調(diào)用之間的不同
在javaScript中,他們只是單個構(gòu)造對象的三種不同的使用模式
函數(shù)調(diào)用
function hello(username){ return "hello, " + username; } hello("keyser Soze");//"hello, keyser Soze"
方法調(diào)用
var obj = { hello: function(){ return "hello, " + this.username; }, username: "hans Gruber" }; obj.hello();//"hello, hans Gruber"http://方法調(diào)用需要對象來調(diào)用,直接調(diào)用就時函數(shù)調(diào)用了 //注意以下情況 var obj2 = { hello: obj.hello, username: "Boo Radley" }; obj2.hello();//"hello, Boo Radley" //方法調(diào)用中,由調(diào)用表達式自身來確定this變量的綁定. 調(diào)用表達式為obj2.hello()所以this指向obj2,而不是obj //此處先略過call,bind,apply
構(gòu)造函數(shù)調(diào)用
function User(name, password){ this.name = name; this.password = password; } var u = new User("sfalken","xxx");//構(gòu)造函數(shù)需要通過new運算符調(diào)用 u.name;//"sfalken" //構(gòu)造函數(shù)調(diào)用一個全新的對象作為this變量的值,并隱式地返回這個新對象作為調(diào)用的結(jié)果,即 產(chǎn)生的新對象傳遞給了u.構(gòu)造函數(shù)的主要職責(zé)時初始化該新對象.
19 - 熟練掌握高階函數(shù)
高階函數(shù): 將函數(shù)作為參數(shù)或返回值的函數(shù).
學(xué)會發(fā)現(xiàn)可以被高階函數(shù)取代的常見編碼模式.
舉例:
[3,1,4,1,5,9].sort(function(x, y){ if(x < y)return -1; if(x > y)return 1; return 0; });//1,1,3,4,5,9 //此處定義了一個判斷數(shù)字大小的方法作為參數(shù)傳遞
20 - 使用call方法自定義接收者來調(diào)用方法
func.call(thisArg [, arg1[,arg2...]])
第一個參數(shù)顯式地提供了接收者對象.
tips:
使用call方法自定義接收者來調(diào)用函數(shù)
使用call方法可以調(diào)用在給定的對象中不存在的方法
-
使用call方法定義高階函數(shù)允許使用者給回調(diào)函數(shù)指定接收者
舉例:
var table = { entries: [], addEntry: function(key, value){ this.entries.push({key:key, value: value}); }, forEach: function(f, thisArg){ var entries = this.entries; for(var i = 0, n = entries.length; i < n; i++){ var entry = entries[i]; //回調(diào)函數(shù)f,并指定接收者為thisArg f.call(thisArg, entry.key, entry.value, i); } } } //如table2賦值table1的內(nèi)容 table1.forEach(table2.addEntry, table2); //如果call的第一個參數(shù)為null或undefined,則內(nèi)部this指向windowcall, apply, bind
call和apply的區(qū)別是參數(shù)提供的方式
bind與call,apply的區(qū)別是,bind只綁定,而call和apply會立即調(diào)用
21- 使用apply 方法通過不同數(shù)量的參數(shù)調(diào)用函數(shù)
func.apply(thisArg, [argsArray])
apply可以處理任意數(shù)量的參數(shù)
apply與call十分類似,區(qū)別就是參數(shù)為參數(shù)數(shù)組傳入,而call從第二個參數(shù)開始有0至多個參數(shù)
22 - 使用arguments創(chuàng)建可變參數(shù)的函數(shù)
JavaScript給給個函數(shù)都隱式提供了一個名為arguments的局部變量.可以用索引獲取方法的每個實參(arguments對象并不是一個數(shù)組),并且該對象還有一個length屬性來指示參數(shù)的個數(shù).
tips:
- 使用隱式的arguments對象實現(xiàn)可變參數(shù)的函數(shù)
- 考慮對可變參數(shù)的函數(shù)提供一個額外的固定元數(shù)的版本,從而使使用者無需借助apply方法
23 - 永遠不要修改arguments對象
function callMethod(obj, method){
var shift = [].shift;
shift.call(arguments);
shift.call(arguments);
return obj[method].apply(obj, arguments);
}
var obj = {
add: function(x, y){return x + y;}
};
callMethod(obj, "add", 17, 25);
//error: cannot read property "apply" of undefined
//arguments對象并不是函數(shù)參數(shù)的副本.所有命名參數(shù)都是arguments對象中對應(yīng)索引的別名
//=> 參數(shù)obj依舊是arguments[0]的別名,method依舊是arguments[1]的別名
//因此此處調(diào)用的不是obj['add']而是17['25']
//=>arguments對象和函數(shù)的命名參數(shù)之間的關(guān)系非常脆弱.所以不建議修改arguments對象
//對比嚴(yán)格模式下
function strict(x){
"use strict";
arguments[0] = "modified";
console.log(arguments[0],x);//modified unmodified
return x === arguments[0];
}
function nostrict(x){
arguments[0] = "modified";
console.log(arguments[0],x);//modified modified
return x === arguments[0];
}
console.log(strict("unmodified"));//false
console.log(nostrict("unmodified"));//true
//非嚴(yán)格模式下,arguments和函數(shù)命名參數(shù)都修改了,而嚴(yán)格模式下,命名參數(shù)沒被修改
//嚴(yán)格模式下,不支持對其arguments對象取別名,即 此處x不是arguments[0].所以嚴(yán)格模式下返回的結(jié)果為false
tips:
使用[].slice.call(arguments)將arguments復(fù)制到一個真正的數(shù)組中再進行修改.
24 - 使用變量保存arguments的引用
//實現(xiàn)一個迭代器
function values(){
var i = 0, n = arguments.length;
return {
hasNext: function(){
return i < n;
},
next: function(){
if(i >= n ){
return;
}
return arguments[i++];
}
}
}
var it = values(1,4,1,4,2,1,3,5,6);
//console.log(it);
console.log(it.next());//undefined
console.log(it.next());//undefined
console.log(it.next());//undefined
//原因是一個新的arguments變量被隱式地綁定到每個函數(shù)體內(nèi)
//it對象的next方法有自己的arguments對象,且為null,打印時就成了undefined
//解決方案:
//在我們需要的arguments對象的作用域中,使用一個變量去綁定,引用這個變量即可
//?感覺可以理解為閉包?訪問作用域內(nèi)的參數(shù),因為it的方法中有本地的arguments對象了,所以會優(yōu)先使用本地的arguments,而此處添加一個引用,在it的方法中不再擁有,就不會出問題了.
function values(){
+ var i = 0, n = arguments.length, a = arguments;
- var i = 0, n = arguments.length;
return {
hasNext: function(){
return i < n;
},
next: function(){
if(i >= n ){
return;
}
+ return a[i++];
- return arguments[i++];
}
}
}
var it = values(1,4,1,4,2,1,3,5,6);
//console.log(it);
console.log(it.next());//undefined
console.log(it.next());//undefined
console.log(it.next());//undefined
tips:
- 當(dāng)引用arguments時當(dāng)心函數(shù)嵌套層級
- 綁定一個明確作用域的引用到arguments變量,從而可以在嵌套的函數(shù)中引用他
25 - 使用bind方法提取具有確定接收者的方法
var buffer = {
entries:[],
add: function(s){
console.log(arguments);//Arguments(3) ["867", 0, Array(3), callee: ?, Symbol(Symbol.iterator): ?]
this.entries.push(s);
},
concat:function(){
return this.entries.join("")
}
};
var source = ["867","-","5309"];
source.forEach(buffer.add);// Cannot read property 'push' of undefined
console.log(buffer.concat());
//原因:此處buffer.add的接收者不是buffer對象
//forEach方法的實現(xiàn)使用全局對象作為默認的接收者,所以此處傳遞給了全局對象,全局對象沒有entries,所以報錯
//arr.forEach(callback(currentValue [, index [, array]])[, thisArg]);
//thisArg為null或者undefined時,this都指向全局對象
//解決方案:
//1.forEach提供了綁定接收者的參數(shù)
source.forEach(buffer.add, buffer);
//2.bind方法綁定this到buffer上
source.forEach(buffer.add.bind(buffer));
26 - 使用bind方法實現(xiàn)函數(shù)柯里化
函數(shù)柯里化: 創(chuàng)建一個固定需求參數(shù)子集的委托函數(shù),即只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個函數(shù)去處理剩下的參數(shù)(11 - 數(shù)量掌握閉包里有)
function simpleURL(protocol, domain, path){
return protocol + "://" + domain + "/" + path;
}
var urls = paths.map(function(path){
return simpleURL("http", siteDomain, path);
});
等效于
var urls = paths.map(simpleURL.bind(null, "http", siteDomain));
//因為不需要引用this變量,所以bind的第一個參數(shù)可以是任何值,使用null或undefined為習(xí)慣用法
27 - 使用閉包而不是字符串來封裝代碼(?現(xiàn)在ES6有塊級作用域了,感覺答案更顯而易見了?)
//這里說的字符串來封裝代碼使用的是eval(str)的方法來執(zhí)行
不使用字符串的原因:
- eval內(nèi)的代碼要執(zhí)行到這一行才會生效,使用的參數(shù)可能跟想象中的有差異.
- 高性能引擎很難優(yōu)化eval字符串里面的代碼
tips:
- 當(dāng)將字符串傳遞給eval函數(shù)以執(zhí)行他們的API時,絕不要在字符串內(nèi)包含局部變量的引用
- 接受函數(shù)調(diào)用的API優(yōu)于使用eval函數(shù)執(zhí)行字符串的API
28 - 不要信賴函數(shù)對象的toString方法
//此處指的是用toString試圖獲取函數(shù)的源代碼
tips:
- 當(dāng)調(diào)用函數(shù)的toString方法時,并沒有要求js引擎能夠精確地獲取到函數(shù)的源代碼
- 由于在不同引擎下調(diào)用的toString方法的結(jié)果可能不同,所以絕不要信賴函數(shù)toString獲得的源代碼的詳細細節(jié).
- toString方法的執(zhí)行結(jié)果并不會暴露存儲在閉包中的局部變量值.
- 通常情況下,避免使用函數(shù)對象的toString方法
(remain learning)29 - 避免使用非標(biāo)準(zhǔn)的棧檢查屬性
許多javaScript環(huán)境提供檢查調(diào)用棧的功能.每個arguments對象都含有兩個額外的屬性,arguments.callee(除了允許匿名函數(shù)遞歸地引用自身之外,沒有用途)和arguments.caller(這個屬性不可靠,大多數(shù)環(huán)境已移除),前者指向使用該arguments對象被調(diào)用的函數(shù),后者指向調(diào)用該arguments對象的函數(shù).
tips:
- 避免使用非標(biāo)準(zhǔn)的arguments.caller和arguments.callee屬性,因為他們不具有良好的移植性.
- 避免使用非標(biāo)準(zhǔn)的函數(shù)對象caller屬性,因為在包含全部棧信息方面,是不可靠的.
- 最好的策略是使用交互式的調(diào)試器.
對象和原型
30 - 理解prototype, getPrototypeOf和_proto_之間的不同
User.prototype指向由new User()創(chuàng)建的對象的原型
Object.getPrototypeOf(user)是ES5中用來獲得user對象的原型對象的標(biāo)準(zhǔn)方法
user._proto_是獲取user對象的原型對象的非標(biāo)準(zhǔn)方法

User構(gòu)造函數(shù)有一個默認的prototype屬性指向原型對象.
由new User()創(chuàng)建的user對象繼承User原型對象.當(dāng)查找user對象屬性時,如果user沒有,就會到他繼承的User原型對象中去找.
ES5提供了Object.getPrototypeOf()的方法去獲取對象的原型對象
Object.getPrototypeOf(user) === User.prototype; //true
一些環(huán)境提供了非標(biāo)準(zhǔn)的方法獲取對象的原型
user.__proto__ === User.prototype; //true
31 - 使用Object.getPrototypeOf函數(shù),而不要使用_proto_屬性
32 - 始終不要修改_proto_屬性
原因:
- _proto_屬性很特殊,他提供了Object.getPrototypeOf方法不具備的修改對象原型鏈接的能力.因為并不是所有平臺都支持改變對象原型的特性,所以有可移植性的問題
- 性能原因.因為現(xiàn)代的js引擎都深度優(yōu)化了獲取和設(shè)置對象屬性的行為,更改對象的內(nèi)部結(jié)構(gòu)(如添加或刪除該對象或其原型鏈中對象的屬性),將會使一些優(yōu)化失效.修改_proto_屬性實際上改變了繼承結(jié)構(gòu)本身,可能是最具破壞性的修改.會導(dǎo)致更多的優(yōu)化失效.
- 為了保持行為的可預(yù)測性.修改對象的原型鏈會交換對象的整個繼承層次結(jié)構(gòu).
tips:
- 使用Object.create函數(shù)給新對象設(shè)置自定義的原型.
33 - 使構(gòu)造函數(shù)與new操作符無關(guān)(ES6引入了class,這部分內(nèi)容應(yīng)該過時了)
//調(diào)用構(gòu)造函數(shù)時,忘記使用new關(guān)鍵字
//這樣函數(shù)的接收者將是全局對象
//會災(zāi)難性地創(chuàng)建全局變量,如果已存在則會修改其值
var u = User("name", "password");
function User(name, password){
//"use strict";
//如果使用嚴(yán)格模式, 則this.name = name;這一行會報錯
this.name = name;
this.password = password;
}
//=>可以修改為以下方式
function User(name, password){
var self = this instanceof User ? this : Object.create(User.prototype);
self.name = name;
self.password = password;
return self;
}
34 - 在原型中存儲方法
好處:
? 將方法存儲在原型中,使其可以被所有的實例使用,減少了占用的內(nèi)存.現(xiàn)代的js引擎深度優(yōu)化了原型查找,所以相對于把方法冗余地寫在實例對象中,并不一定能保證在查找速度上有明顯的提升.
35 - 使用閉包存儲私有數(shù)據(jù)
閉包時一種簡樸的數(shù)據(jù)結(jié)構(gòu).他們將數(shù)據(jù)存儲到封閉的變量中而不提供對這些變量的直接訪問.獲取閉包內(nèi)部結(jié)構(gòu)的唯一方式是該函數(shù)顯式地提供獲取它的途徑.
//該實現(xiàn)以變量的方式來引用name和passwordHash變量,而不是this屬性的方式
//此時User的實例中不會包含任何實例屬性,因此外部的代碼不能直接訪問User的實例的name和passwordHash變量
function User(name, passwordHash){
this.toString = function(){return `[User ${name}]`;}
this.checkPassword = function(password){
return hash(password) === passwordHash;
}
}
//缺點:
//為了使構(gòu)造函數(shù)中的變量在使用他們的方法的作用域內(nèi),那些方法必須置于實例對象中
//即不能用繼承的方式去減少占用內(nèi)存了.因為對象內(nèi)部根本沒有name和passwordHash這兩個屬性了.僅實例中定義的方法能訪問這兩個變量
tips:
- 閉包的變量是私有的,只能通過局部的引用獲取
- 將局部變量作為私有屬性從而通過方法實現(xiàn)信息隱藏.
36 - 只將實例狀態(tài)存儲在實例對象中
有狀態(tài)的數(shù)據(jù)可以存儲在原型中,只要你真的想要共享它.
一般情況下,原型對象中共享方法,而每個實例的狀態(tài)存儲在各自的實例對象中.
37 - 認識到this變量的隱式綁定問題
tips:
- this變量的作用域總是由其最近的封閉函數(shù)所確定.
- 使用一個局部變量(通常為self,me或that)綁定this,然后在內(nèi)部函數(shù)中使用.
38 - 在子類的構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù)(ES6引入了class,這部分內(nèi)容應(yīng)該過時了)
tips:
- 在子類的構(gòu)造函數(shù)中,顯示傳入this作為顯示的接收者調(diào)用父類的構(gòu)造函數(shù)
- 使用Object.create函數(shù)來構(gòu)造子類的原型對象以避免調(diào)用父類的構(gòu)造函數(shù)
//Object.create()方法創(chuàng)建一個新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的__proto__
Son.prototype = Object.create(Father.prototype);
39 - 不要重用父類的屬性名
40 - 避免繼承標(biāo)準(zhǔn)類
ECMAScript標(biāo)準(zhǔn)庫內(nèi)的類定義了很多特殊的行為,很難寫出行為正確的子類.
ECMAScript定義的完整的[[Class]]屬性值集合:
| [[Class]] | Construction |
|---|---|
| "Array" | new Array(…), [...] |
| "Boolean" | new Boolean(...) |
| "Date" | new Date(...) |
| "Error" | new Error(...), new EvalError(...), new RangeError(...), new ReferenceError(...), new SyntaxError(...), new TypeError(...), new URIError(...) |
| "Function" | new Function(...), function (...){...} |
| "JSON" | JSON |
| "Math" | Math |
| "Number" | new Number(...) |
| "Object" | new Object(...), {...}, new MyClass(...) |
| "RegExp" | new RegExp(...), /.../ |
| "String" | new String(...) |
通過右側(cè)的構(gòu)造函數(shù)創(chuàng)建的才能有[[Class]]這個內(nèi)部屬性.
因為[[Class]]屬性及其他特殊的內(nèi)部屬性導(dǎo)致繼承這些類總會有問題,因此,最好避免繼承: Array, Boolean, Date, Function, Number, RegExp或 String.
Math對象所有屬性和方法都是靜態(tài)的,可對其作拓展.
JSON對象只有parse()和stringify()兩個方法,除了這兩個方法外,對象本身并無其他作用,也不能被調(diào)用或作為構(gòu)造函數(shù)調(diào)用.
Error, Object常被繼承.
tips:
- 使用屬性委托優(yōu)于繼承標(biāo)準(zhǔn)類.
41 - 將原型視為實現(xiàn)細節(jié)
因為對一個對象的操作,并不需要在意屬性或方法是在原型繼承結(jié)構(gòu)的那個位置,修改原型上的方法/屬性,將會影響依賴這個原型的對象.
tips:
- 對象是接口,原型是實現(xiàn)(即原型實現(xiàn)了實際的功能, 我們調(diào)用繼承這個原型的對象,這個對象就相當(dāng)于一個接口)
- 避免檢查你無法控制的對象的原型結(jié)構(gòu)
- 避免檢查實現(xiàn)在你無法控制的對象內(nèi)部的屬性
42 - 避免使用輕率的猴子補丁
猴子補丁(monkey-patching): 對象共享原型,所以每一個對象都可以增加、刪除或修改原型的屬性,這個操作被稱為猴子補丁.
使用場景:polyfill.即在某些js環(huán)境中,使用舊的方式實現(xiàn)新的API.
舉例:
//先判斷是否有這個方法
if(typeof Array.prototype.map !== "function"){
//沒有這個方法的話,為之定義
Array.prototype.map = function(f, thisArg){
var result = [];
for(var i = 0, n = this.length; i < n; i++){
result[i] = f.call(thisArg, this[i], i);
}
return result;
};
}
tips:
避免輕率地使用猴子補丁.
記錄程序庫所執(zhí)行的所有猴子補丁.
-
考慮通過將修改置于一個導(dǎo)出函數(shù)中,使猴子補丁稱為可選(即可以選擇執(zhí)行這個方法,也可選擇執(zhí)行其他的,而不是直接打在原型上)
如:
//置于函數(shù)中,用戶可以選擇調(diào)用該函數(shù)或忽略 //如果忽略,Array原型上就不具有該方法,調(diào)用,則具有 //這樣就不會有多處定義導(dǎo)致沖突的問題 function addArrayMethods(){ Array.prototype.split = function(i){ return [this.slice(0,i), this.slice(i)]; }; }; 使用猴子補丁為缺失的API提供polyfills.
數(shù)組和字典
43 - 使用Object的直接實例構(gòu)造輕量級的字典
輕量級字典的首要原則: 使用Object的直接實例作為字典,而不是其子類或者數(shù)組.這樣不容易遇到原型污染的問題(因為原型污染的范圍縮小到僅有Object.prototype).并且我們本身也建議不要直接添加屬性到Object.prototype中.
使用for...in循環(huán)去枚舉字典的屬性.
44 - 使用null原型以防止原型污染
防止原型污染最簡單的方式之一就是一開始就不使用原型.
ES5:
var o = Object.create(null);
Object.getPrototypeOf(o) === null;//true
存在的問題:
-
在不同環(huán)境中,還是會有一些問題.
理想中,空對象是不存在_proto_屬性的. in和hasOwnProperty都應(yīng)該返回false
而在一些環(huán)境中(只有in操作符為true)
"xxx" in o //true
{}.hasOwnProperty.call(o, "_proto_") //false
而在一些環(huán)境中,會存在一個實例屬性_proto_
"xxx" in o //true
{}.hasOwnProperty.call(o, "_proto_") //true
ES5前不支持Object.create函數(shù)的環(huán)境:
var x = {__proto__: null};
x instanceof Object;//false
存在的問題:
- _proto_既不標(biāo)準(zhǔn),也不是完全可移植的,并且在未來可能被移除
- 如果使用_proto_作為屬性名,可能會導(dǎo)致一些問題,因為一些環(huán)境中把他作為特殊的屬性對待.
=>推薦45中的方式定義字典.
45 - 使用hasOwnProperty方法避免原型污染
一種避免原型污染的字典的有效實踐
//為了達到最大的可移植性和安全性,對__proto__屬性做了特殊處理
function Dict(elements){
this.elements = elements || {};
//是否設(shè)置了__proto__屬性的標(biāo)識符
this.hasSpecialProto = false;
//用于存儲設(shè)置的__proto__的值
this.specialProto = undefined;
}
Dict.prototype.has = function(key){
if(key === "__proto__"){
return this.hasSpecialProto;
}
//使用{}.hasOwnProperty的方式,避免原型污染
//普通的類,很可能因為原型定義了hasOwnProperty的方法導(dǎo)致原型污染
return {}.hasOwnProperty.call(this.elements, key);
}
Dict.prototype.get = function(key){
if(key === "__proto__"){
return this.specialProto;
}
return this.has(key) ? this.elements[key] : undefined;
}
Dict.prototype.set = function(key, val){
if(key === "__proto__"){
this.hasSpecialProto = true;
this.specialProto = val;
}else{
this.element[key] = val;
}
}
Dict.prototype.remove = function(key){
if(key === "__proto__"){
this.hasSpecialProto = false;
this.specialProto = undefined;
}else{
delete this.elements[key];
}
}
46 - 使用數(shù)組而不要使用字典來存儲 有序 集合
原因: JavaScript對象是一個無序的屬性集合,獲得和設(shè)置不同的屬性與順序無關(guān),與js引擎有關(guān).
tips:
- 使用for...in循環(huán)來枚舉對象的屬性應(yīng)當(dāng)與順序無關(guān)
- 如果聚集運算字典中的數(shù)據(jù),確保聚集操作與順序無關(guān)(其實也就是不要用字典來存儲有序的數(shù)據(jù))
- 使用數(shù)組而不是字典來存儲有序集合
47 - 絕不要在Object.prototype中增加可枚舉的屬性
原因:在Object.prototype中添加可枚舉的屬性,會污染所有字典的枚舉遍歷.所以避免在Object.prototype中增加屬性
折中方式:
-
考慮編寫一個函數(shù)代替Object.prototype
對象中的函數(shù)就叫做方法,通過"."的方式調(diào)用
-
ES5后提供的方法,定義不可枚舉的屬性
Object.defineProperty(Object.prototype, "allKeys", { value: function(){ var result = []; for(var key in this){ result.push(key); } return result; }, writable: true, enumerable: false,//這一項就是定義allKeys是不可枚舉的,for...in時不會出現(xiàn)它 configurable: true })
48 - 避免在枚舉期間修改對象
原因:for...in遍歷枚舉的期間,添加新的屬性,不能保證在枚舉期間能訪問到新添加的屬性.
tips:
- 使用for...in循環(huán)枚舉一個對象的屬性時,確保不要修改該對象.
- 迭代一個對象時,如果對象的內(nèi)容在循環(huán)期間可能會改變,應(yīng)該用while循環(huán)或經(jīng)典for循環(huán)來代替for...in 循環(huán).
- 為了在不斷變化的數(shù)據(jù)結(jié)構(gòu)中能夠預(yù)測枚舉,考慮使用一個有序的數(shù)據(jù)結(jié)構(gòu),如數(shù)組,而不是字典對象.
49 - 數(shù)組迭代要優(yōu)先使用for循環(huán)而不是for...in循環(huán)
var scores = [98,74,85,77,93,100,89];
var total = 0;
for(var score in scores){
total += score;
}
var mean = total / scores.length;//此時的total其實是00123456
console.log(mean);//17636.571428571428而不是88
//正確的操作方式
//且在一開始就計算出數(shù)組的長度,避免寫在中間,那樣每次迭代都會計算一次數(shù)組長度
for(var i = 0, n = scores.length; i < n; i++){
total += scores[i];
}
50 - 迭代方法優(yōu)于循環(huán)
原因:常見的終止條件的錯誤.
循環(huán)相對于迭代函數(shù)的唯一優(yōu)勢: 有控制流操作,如break和continue.
如下:
for(var i = 0; i <= n; i++){...}//尾部多了一次循環(huán)
for(var i = 1; i < n; i++){...}//頭部少了第一次循環(huán)
for(var i = n; i >= 0; i--){...}//多了頭部的一次循環(huán)
for(var i = n - 1; i > 0; i--){...}//少了最后一次循環(huán)
常用模式1:Array.prototype.forEach
代碼簡單可讀并且消除了終止條件和任何數(shù)組索引
for(var i = 0, n = players.length; i < n; i++){
players[i].score++;
}
//等效于
players.forEach(function(p){
p.score++;
})
常用模式2: Array.prototype.map
對數(shù)組每個元素進行一些操作后建立一個新的數(shù)組
var trimmed = [];
for(var i = 0, n = input.length; i < n; i ++){
trimmed.push(input[i].trim());
}
//等效于
var trimmed = input.map(function(s){
return s.trim();
})
常用模式3:Array.prototype.filter
計算一個新的數(shù)組,數(shù)組中的元素為現(xiàn)有數(shù)組中的一部分
var newArr = listings.filter(function(listing){
return listing.price >= min && listing.price <= max;//return true的值會被留下,返回false的就不存在返回的新數(shù)組中了
})
可以提前終止循環(huán)的模式:
Array.prototype.some
返回一個布爾值表示其回調(diào)函數(shù)對數(shù)組的任何一個元素是否返回了真值
一旦產(chǎn)生真值就返回
[1,10,100].some(function(x){return x > 5;});//true
[1,10,100].some(function(x){return x < 0;});//false
Array.prototype.every
返回一個布爾值表示其回調(diào)函數(shù)是否對所有元素返回了一個真值
一旦產(chǎn)生假值就返回
[1,2,3,4,5].every(function(x){return x > 0;}); //true
[1,2,3,4,5].every(function(x){return x < 3;}); //false
tips:
- 使用迭代方法替換for循環(huán)增加代碼可讀性,避免思考重復(fù)循環(huán)的邏輯控制(即邊界條件)
- 使用自定義迭代函數(shù)來抽象常見循環(huán)模式(注意猴子補丁的注意事項,見42)
- 在存在需要提前終止的情況下,仍推薦傳統(tǒng)的循環(huán)方式,避免不必要的循環(huán).另外,some和every也可用于提前退出.
51 - 在類數(shù)組對象上復(fù)用通用的數(shù)組方法
類數(shù)組對象的規(guī)則:
- 具有一個范圍在0~2^32-1的整型length屬性
- length屬性大于該對象的最大索引.索引是一個范圍在0~2^32-2的整數(shù),它的字符串表示的是該對象中的一個key
滿足上述兩個條件的類數(shù)組對象使用Array.prototype任一方法都可兼容(除了concat方法必須是數(shù)組對象才行).
使用方式:
[].(forEach/some/...).call(類數(shù)組對象, ...);
常見類數(shù)組
- 函數(shù)的arguments對象
- DOM的NodeList類,如:document.getElementsByTagName,document.querySelectorAll獲得的對象
- 自定義的簡單對象字面量 如:var arrayLike = {0: "a",1:"b",2:"c",length:3};
- 字符串
轉(zhuǎn)換類數(shù)組為真正的數(shù)組對象的方法
- [].slice.call(類數(shù)組對象)
- Array.from(類數(shù)組對象/可迭代對象Set,Map)
52 - 數(shù)組字面量優(yōu)于數(shù)組構(gòu)造函數(shù)
var a = [1,2,3,4,5];
//優(yōu)于
var a = new Array(1,2,3,4,5);
好處:
- 使用構(gòu)造函數(shù)的方式需要先確認Array沒被包裝過
- 使用構(gòu)造函數(shù)的方式需要確認沒人修改過全局的Array變量
//以上兩點應(yīng)該不太可能發(fā)生...
-
使用單個數(shù)組時,有明顯不同
var a = [17];和var a = new Array(17);
后者會創(chuàng)建一個17長度的數(shù)組!
庫和API設(shè)計
53 - 保持一致的約定
tips:
- 在變量命名和函數(shù)簽名中使用一致的約定.如width,height等
- 不要偏移用戶在他們開發(fā)平臺中很可能遇到的約定.如width,height的順序等等
54 - 將undefined看做"沒有值"(缺少某個特定的值)
出現(xiàn)undefined的情況
- 未賦值的變量初始值為undefined
- 訪問對象中不存在的屬性,會產(chǎn)生undefined
- 函數(shù)體尾使用未帶參數(shù)的return ;或者未使用return都會產(chǎn)生返回值undefined
- 未給函數(shù)提供實參則該函數(shù)參數(shù)值為undefined
可選參數(shù)的實現(xiàn)的常見做法(不要檢查arguments.length,下面兩種方式更健壯)
-
測試undefined
function Server(port, hostname){ if(hostname === undefined){ hostname = "localhost"; } hostname = String(hostname); // ... } -
測試是否為真
function Server(Port, hostname){ hostname = String(hostname || "localhost"); // ... }邏輯運算符或(||),當(dāng)?shù)谝粋€參數(shù)為真時返回第一個參數(shù),否則返回第二個參數(shù)
不適合使用"測試是否為真"的情況是:
? 如果除了undefined的其他假值(false, 0, -0, null, NaN, "")可以為函數(shù)的合法值時,就不能使用這種方式.需要選用上一種方式
55 - 接收關(guān)鍵字參數(shù)的選項對象
tips:
-
使用選項對象使得API更具可讀性、更容易記憶;且參數(shù)都是可選的,調(diào)用者可提供任一可選參數(shù)的子集.
- 原因: 當(dāng)參數(shù)過多時,位置參數(shù)的方式可讀性很差
-
所有通過選項對象提供的參數(shù)應(yīng)當(dāng)被視為可選的
function Alert(parent, message, opts){//必選的parent和message需要抽離出來 opts = opts || {};//如果不傳入,初始化一個空對象避免后面的操作報錯 this.width = opts.width == undefined ? 320: opts.width;//數(shù)字參數(shù) 0一般是合法值,所以要用undefined判斷 this.height = opts.height == undefined ? 240: opts.height; this.title = opts.title || "Alert";//字符串一般空串不會算作合法值,所以這里用或操作符,如果此處空串也為合法值,那應(yīng)該改為測試undefined this.modal = !!opts.modal;//布爾值,使用雙重否定將參數(shù)強制轉(zhuǎn)換成一個布爾值 } -
如果使用其他庫,可以使用extend函數(shù)抽象出從選項對象中提取值的邏輯(前提是庫/框架提供了這個函數(shù))
//一個典型的extend實現(xiàn) function extend(target, source){ if(source){ for(var key in source){ var val = source[key]; if(typeof val !== "undefined"){ target[key] = val; } } } return target; }//經(jīng)過extend函數(shù)簡化 function Alert(parent, message, opts){ opts = extend({ width:320, height:240 }); opts = extend({ title: "Alert", modal: false }, opts); this.width = opts.width; this.height = opts.height; this.title = opts.title; this.modal = opts.modal; //如果options是整個復(fù)制到this對象,可以進一步簡化上面四行 extend(this, opts); }
56 - 避免不必要的狀態(tài)(狀態(tài)有時候是必須的)
區(qū)別:無狀態(tài)函數(shù)或方法的行為只取決于輸入,與程序的狀態(tài)改變無關(guān)
好處: 更容易學(xué)習(xí)和使用,更清晰(簡潔),更不易出錯; 相比于有狀態(tài)的API,無狀態(tài)的API會自動重用默認值,我們就不用擔(dān)心默認值是否在前面某處被更改了.
tips:
- 盡可能地使用無狀態(tài)的API
- 如果API是有狀態(tài)的,需要標(biāo)示出每個操作與哪些狀態(tài)有關(guān)聯(lián)
57 - 使用結(jié)構(gòu)類型structural typing(鴨子類型duck typing)設(shè)計靈活的接口
區(qū)別于一般面向?qū)ο笳Z言推薦的使用類和繼承來結(jié)構(gòu)化程序, javaScript動態(tài)類型使用的方式更靈活.只要有預(yù)期的結(jié)構(gòu)即可.
Wiki.formats.MEDIAWIKI = function(source){
//extract contents from source
//...
return {
getTitle: function(){/* ... */},
getAuthor: function(){/* ... */},
toHTML: function(){/* ... */}
}
}
我們完全可以根據(jù)每種格式的需要,混合和匹配功能
繼承有時候?qū)е卤人鉀Q的問題外更多的問題.
靜態(tài)語言迫使必須符合某個數(shù)據(jù)類型,增加了更多的代碼;而動態(tài)語言可以編寫更少的代碼,使代碼更簡潔,可以花更多的精力在業(yè)務(wù)邏輯上.缺點是無法保證變量類型,可能在運行時出錯.
鴨子類型是典型的面向接口編程,且不必借助超類的幫助.它只要正確的實現(xiàn)了我們需要的方法供我們調(diào)用即可.
tips:
- 使用結(jié)構(gòu)類型來設(shè)計靈活的對象接口
- 結(jié)構(gòu)接口更靈活,更輕量,所以應(yīng)該避免使用繼承
- 針對單元測試,使用mock對象即接口的替代實現(xiàn)來提供可復(fù)驗的行為.
58 - 區(qū)分?jǐn)?shù)組對象和類數(shù)組對象
tips:
- 絕不重載與其他類型有重疊的結(jié)構(gòu)類型
- 當(dāng)重載一個結(jié)構(gòu)類型與其他類型時,先測試其他類型.因為結(jié)構(gòu)類型沒有明確的信息標(biāo)記他們實現(xiàn)的結(jié)構(gòu)類型,沒有可靠的編程方法來檢測該信息.
- 當(dāng)重載其他對象類型時,接受真數(shù)組而不是類數(shù)組對象
- 文檔標(biāo)注你的API是否接受真數(shù)組或類數(shù)組值
- 使用ES5提供的Array.isArray方法測試真數(shù)組(比instance of操作符更可靠)
59 - 避免過度的強制轉(zhuǎn)換
tips:
- 避免強制轉(zhuǎn)換和重載混用
- 考慮防御性地監(jiān)視非預(yù)期的輸入
- 防御性編程:試圖以額外的檢查來抵御潛在的錯誤
60 - 支持方法鏈(就是函數(shù)式編程)
tips:
- 使用方法鏈來連接無狀態(tài)的操作.
- 通過在無狀態(tài)的方法中返回新對象來支持方法鏈
- 通過在有狀態(tài)的方法中返回this來支持方法鏈
并發(fā)
61 - 不要阻塞I/O事件隊列
系統(tǒng)維護了一個按事件發(fā)生順序排列的內(nèi)部事件隊列,一次調(diào)用一個已注冊的回調(diào)函數(shù),在執(zhí)行過程中,系統(tǒng)會適時地查看處理進度,會在異步執(zhí)行結(jié)束后立刻調(diào)用回調(diào)函數(shù).
好處:系統(tǒng)的這種運行方式有時被稱為運行到完成機制擔(dān)保(run-to-completion),當(dāng)代碼運行時,你完全可以掌握應(yīng)用程序的狀態(tài),不用擔(dān)心一些變量和對象的屬性由于并發(fā)執(zhí)行代碼而超出你的控制.
不足:實際上這些異步執(zhí)行的代碼支撐著后續(xù)應(yīng)用程序的執(zhí)行(返回某些重要的參數(shù),又會作為后面代碼的實參等等).在客戶端,一個阻塞的事件處理程序會阻塞任何將被處理的其他用戶輸入,甚至阻塞頁面渲染;在服務(wù)器端,一個阻塞的事件處理程序可能會阻塞其他將被處理的網(wǎng)絡(luò)請求,導(dǎo)致服務(wù)器失去響應(yīng).
tips:
- 異步API使用回調(diào)函數(shù)來延緩處理代價高昂的操作以避免阻塞主應(yīng)用程序.
- JavaScript并發(fā)地接收事件,但會使用一個事件隊列按序地處理事件.
- 在應(yīng)用程序事件隊列中絕不要使用阻塞的I/O.
62 - 在異步序列中使用嵌套或命名的回調(diào)函數(shù)
為了保證存在依賴的異步代碼的執(zhí)行順序,使用嵌套或命名的回調(diào)函數(shù).
tips:
使用嵌套或命名的回調(diào)函數(shù)按順序地執(zhí)行多個異步操作.
嘗試在過多的嵌套的回調(diào)函數(shù)和尷尬的命名的非嵌套回調(diào)函數(shù)中取得平衡(后面會介紹解決該問題的ES6新特性Promise)
避免將可被并行執(zhí)行的操作順序化.
63 - 當(dāng)心丟棄錯誤
常見的異步處理錯誤:
//1.定義一個附加的錯誤處理回調(diào)函數(shù)(errbacks)
downloadAsync("http://example.com/file.txt",function(text){
console.log(text);
}, function(error){
console.log("Error:" + error);
})
//2.異步操作出錯則回調(diào)函數(shù)返回的第一個參數(shù)為真,沒出錯則為一個假值
//常用在Node.js平臺
function onError(error){
console.log("Error:" + error);
}
downloadAsync("a.txt", function(error, a){
if(error) return onError(error);
downloadAsync("b.txt", function(error, b){
if(error) return onError(error);
console.log("contents:" + a + b);
})
})
tips:
- 通過編寫共享的錯誤處理函數(shù)來避免賦值和粘貼錯誤處理代碼
- 確保明確地處理所有的錯誤條件以避免丟失錯誤.
64 - 對異步循環(huán)使用遞歸
tips:
- 循環(huán)不能是異步的.
- 使用遞歸函數(shù)在事件循環(huán)的單獨輪次中執(zhí)行迭代.
- 在事件循環(huán)的單獨輪次中執(zhí)行遞歸,并不會導(dǎo)致調(diào)用棧溢出.
65 - 不要在計算時阻塞事件隊列
tips:
- 避免在主事件隊列中執(zhí)行代價高昂的算法. (如:使用setTimeout的方式變?yōu)楫惒綀?zhí)行)
- 在支持Worker API 的平臺, 該API可以用在一個獨立的事件隊列中運行長計算程序.
- 在Worker API不可用或代價昂貴的環(huán)境中, 考慮將計算程序分解到事件循環(huán)的多個輪次中.
66 - 使用計數(shù)器來執(zhí)行并行操作
tips:
- 并行操作的結(jié)果順序是不可預(yù)測的,這個時候我們可以使用計算器去確定代碼的執(zhí)行進度.
67 - 絕不要同步地調(diào)用異步的回調(diào)函數(shù)
舉例:
//調(diào)用方代碼
downloadCachingAsync("file.txt", function(file){
console.log("finished");//might happen first
})
console.log("starting");
//被調(diào)用方,定義的函數(shù)
function downloadCachingAsync(url, onsuccess, onerror){
if(cache.has(url)){//模擬有緩存的情況,這種情況代碼走到了同步回調(diào)中
onsuccess(cache.get(url));//此處同步調(diào)用了回調(diào)!!!
return;
//正確的做法
//return setTimeout(onsuccess(cache.get(url),0));
}
return downloadAsync(url, function(file){
cache.set(url, file);
onsuccess(file);
}, onerror);
}
存在的問題:
- 如上述代碼, 同步的調(diào)用異步的回調(diào)函數(shù),導(dǎo)致日志消息竟然出現(xiàn)了錯誤的順序("finish"先于"starting")
- 如64條所述, 異步回調(diào)本質(zhì)上以空的調(diào)用棧來調(diào)用, 因此異步的循環(huán)實現(xiàn)為遞歸函數(shù)時安全的.但是如果同步地調(diào)用不能保障這一點,會使得表面上是異步的循環(huán)耗盡棧空間.
tips:
- 即使可以立即得到數(shù)據(jù),也絕不要同步地調(diào)用異步回調(diào)函數(shù)
- 同步地調(diào)用異步回調(diào)函數(shù)擾亂了操作的序列,可能導(dǎo)致意想不到的交錯代碼
- 同步地調(diào)用異步的回調(diào)可能導(dǎo)致棧溢出或錯誤地處理異常
- 使用異步的API.如setTimeout函數(shù)異步回調(diào)函數(shù)
68 - 使用promise模式清潔異步邏輯
基于promise的API不接收回調(diào)函數(shù)作為參數(shù),而是返回一個promise對象,通過自身的then方法接收回調(diào)函數(shù).
tips:
- promise代表最終值,即并行操作完成時最終產(chǎn)生的結(jié)果.
- 使用promise組合不同的并行操作
- 使用promise模式的API避免數(shù)據(jù)競爭
- 在要求有意的競爭條件時使用select(也被稱為choose)