摘要:語言缺陷是一門在極短時(shí)間里創(chuàng)造的腳本語言,它存在很多的不足,這使得在學(xué)習(xí)時(shí)無形加大了學(xué)習(xí)的難度,本文就將這些內(nèi)容進(jìn)行總結(jié),以防繼續(xù)掉坑。
JS語言缺陷
js是一門在極短時(shí)間里創(chuàng)造的腳本語言,它存在很多的不足,這使得在學(xué)習(xí)時(shí)無形加大了學(xué)習(xí)的難度,本文就將這些內(nèi)容進(jìn)行總結(jié),以防繼續(xù)掉坑。1.變量提升 1.1 案例分析
先來說一下變量提升,它其實(shí)就是先用后聲明,經(jīng)常被拿來說明的一個(gè)例子是:
console.log(a); var a = 10;//undefined
這是由于這段代碼在執(zhí)行的時(shí)候,js解析器會(huì)先把var聲明放在前面,然后順序執(zhí)行對(duì)應(yīng)的語句,執(zhí)行到console的時(shí)候,由于a變量已經(jīng)聲明提升但未進(jìn)行賦值操作,在js中這種情況就會(huì)報(bào)undefined
上面是對(duì)出錯(cuò)的解釋,接下來就細(xì)細(xì)說明一下變量提升的具體內(nèi)容
先來說一下什么是變量,變量就是存放數(shù)據(jù)的空間,在這個(gè)空間里,可以存放具體的數(shù)據(jù),也可以存放數(shù)據(jù)對(duì)應(yīng)的地址,這實(shí)際上是對(duì)應(yīng)數(shù)據(jù)結(jié)構(gòu)中的堆棧,棧數(shù)據(jù)少可以直接將數(shù)據(jù)存放進(jìn)來,堆數(shù)據(jù)多,所以另開空間存放,然后把數(shù)據(jù)對(duì)應(yīng)的內(nèi)存地址放在棧內(nèi),在賦值時(shí),棧類型的數(shù)據(jù)會(huì)直接把數(shù)據(jù)拷貝一份然后進(jìn)行賦值,而堆類型的數(shù)據(jù)會(huì)把地址復(fù)制一份,然后不同的變量會(huì)指向同一個(gè)地址,在js中對(duì)象,函數(shù),數(shù)組等都是堆類型數(shù)據(jù),也叫引用類型數(shù)據(jù),下面直接在控制臺(tái)寫個(gè)小例子看看:
//基本類型 //ab彼此修改值的時(shí)候相互不影響 var a = 1,b; b=a; console.log(a,b)//1 1 a = 2; console.log(a,b)//2,1
//引用類型 //obj修改值時(shí)會(huì)相互影響 var obj1 = new Object(); var obj2 = new Object(); obj1.name="kk" obj2=obj1 obj2.sex="male" console.log(obj1)//{name: "kk", sex: "male"}
弄清楚了堆棧的區(qū)別,就可以來繼續(xù)看變量提升的問題了,在js中變量包括基本的數(shù)據(jù)類型和引用的數(shù)據(jù)類型,并且function被設(shè)置為一等公民,也就是說,在聲明變量時(shí)函數(shù)變量的等級(jí)比其他變量的等級(jí)高,函數(shù)的創(chuàng)建又有兩種方式一種是函數(shù)聲明,另一種是函數(shù)表達(dá)式,在變量提升的時(shí)候,只會(huì)提升聲明而不會(huì)提升表達(dá)式:
//函數(shù)聲明 function say(){ console.log("saying"); } //函數(shù)表達(dá)式 var say = function(){ console.log("saying"); }
到這里先來總結(jié)一下,為了理解變量提升,先了解了變量是什么,變量類型有哪些,函數(shù)創(chuàng)建的形式有什么,接下來就可來檢驗(yàn)一下,看是否真的懂了:
var name = "kk"; function say(){ console.log(name); //輸出:undefined var name = "zoe"; console.log(name); //輸出:"zoe" } say();
來解釋一下為什么:
//1.var name; //2.發(fā)現(xiàn)有函數(shù)聲明,函數(shù)等級(jí)高所以function say();var name; //3.function say();var name;var name; //4.say()調(diào)用函數(shù) //5.此時(shí)name聲明未賦值,所以是undefined //6.var name = "zoe" //7.由于此時(shí)name被賦值了,直接打印zoe //8.var name = "kk"
再來看一個(gè)例子:
var say = function(){ console.log("1"); }; function say(){ console.log("2"); }; say(); //輸出:"1"
說一下為什么:
//1.var say; //2.函數(shù)聲明比變量聲明等級(jí)高,所以function say();var say; //3.function say()聲明未賦值 //4.var say = function(){}賦值 //5.console.log(1) //6.function say()賦值1.2編譯器和解析器
為什么會(huì)出現(xiàn)這種現(xiàn)象呢?從js代碼到瀏覽器識(shí)別js代碼發(fā)生了什么?這個(gè)涉及到編譯原理,大概分成兩個(gè)部分,一是將js代碼生成AST樹,二是將AST樹變成瀏覽器能理解的內(nèi)容,前者叫編譯器,后者叫解釋器,如果自己來設(shè)計(jì),你會(huì)如何處理js代碼呢?這里提供一種思路,那就是把所有的js代碼的信息都記錄下來,然后把他生成一個(gè)樹狀結(jié)構(gòu),也就是我們所說的AST樹,這樣說太抽象了,舉例看看:
//js代碼 if (1 > 0) { alert("aa"); }
//ast樹 { "type": "Program", "start": 0, "end": 29, "body": [ { "type": "IfStatement", "start": 0, "end": 29, "test": { "type": "BinaryExpression", "start": 4, "end": 9, "left": { "type": "Literal", "start": 4, "end": 5, "value": 1, "raw": "1" }, "operator": ">", "right": { "type": "Literal", "start": 8, "end": 9, "value": 0, "raw": "0" } }, "consequent": { "type": "BlockStatement", "start": 11, "end": 29, "body": [ { "type": "ExpressionStatement", "start": 15, "end": 27, "expression": { "type": "CallExpression", "start": 15, "end": 26, "callee": { "type": "Identifier", "start": 15, "end": 20, "name": "alert" }, "arguments": [ { "type": "Literal", "start": 21, "end": 25, "value": "aa", "raw": ""aa"" } ] } } ] }, "alternate": null } ], "sourceType": "module" }
可以在 https://astexplorer.net/ 試試
先來定個(gè)任務(wù),那就是只實(shí)現(xiàn)解析if (1 > 0) {alert("aa");}這句話,因?yàn)閖s的內(nèi)容太多了,所以只實(shí)現(xiàn)上面這句話從js-ast-執(zhí)行,再次聲明,其他所有可能存在的問題都不考慮,只是完成解析上面的一句話,開始:
這句話對(duì)于計(jì)算機(jī)來說就是個(gè)字符串,那如何識(shí)別它呢?首先把這句話拆分,然后把拆分的內(nèi)容組合,這個(gè)實(shí)際叫做詞法解析和語法組合,生成對(duì)應(yīng)的類型和值,那這句話中有什么?
1."if" 2." " 3."(" 4."1" 5." " 6.">" 7." " 8."0" 9.")" 10." " 11."{" 12." " 13."alert" 14."(" 15."aa" 16.")" 17.";" 18." " 19."}"
知道了有什么,就可以開始解析,把他們對(duì)應(yīng)的類型和值標(biāo)好,具體看代碼:
function tokenizeCode(code) { var tokens = []; // 保存結(jié)果數(shù)組 for (var i = 0; i < code.length; i++) { // 從0開始 一個(gè)個(gè)字符讀取 var currentChar = code.charAt(i); if (currentChar === ";") { tokens.push({ type: "sep", value: currentChar }); // 該字符已經(jīng)得到解析了,直接循環(huán)下一個(gè) continue; } if (currentChar === "(" || currentChar === ")") { tokens.push({ type: "parens", value: currentChar }); continue; } if (currentChar === "{" || currentChar === "}") { tokens.push({ type: "brace", value: currentChar }); continue; } if (currentChar === ">" || currentChar === "<") { tokens.push({ type: "operator", value: currentChar }); continue; } if (currentChar === """ || currentChar === """) { // 如果是單引號(hào)或雙引號(hào),表示一個(gè)字符的開始 var token = { type: "string", value: currentChar }; tokens.push(token); var closer = currentChar; // 表示下一個(gè)字符是不是被轉(zhuǎn)譯了 var escaped = false; // 循環(huán)遍歷 尋找字符串的末尾 for(i++; i < code.length; i++) { currentChar = code.charAt(i); // 將當(dāng)前遍歷到的字符先加到字符串內(nèi)容中 token.value += currentChar; if (escaped) { // 如果當(dāng)前為true的話,就變?yōu)閒alse,然后該字符就不做特殊的處理 escaped = false; } else if (currentChar === "") { // 如果當(dāng)前的字符是 , 將轉(zhuǎn)譯狀態(tài)變?yōu)閠rue,下一個(gè)字符不會(huì)被做處理 escaped = true; } else if (currentChar === closer) { break; } } continue; } // 數(shù)字做處理 if (/[0-9]/.test(currentChar)) { // 如果數(shù)字是以 0 到 9的字符開始的話 var token = { type: "number", value: currentChar }; tokens.push(token); // 繼續(xù)遍歷,如果下一個(gè)字符還是數(shù)字的話,比如0到9或小數(shù)點(diǎn)的話 for (i++; i < code.length; i++) { currentChar = code.charAt(i); if (/[0-9.]/.test(currentChar)) { // 先不考慮多個(gè)小數(shù)點(diǎn) 或 進(jìn)制的情況下 token.value += currentChar; } else { // 如果下一個(gè)字符不是數(shù)字的話,需要把i值返回原來的位置上,需要減1 i--; break; } } continue; } // 標(biāo)識(shí)符是以字母,$, _開始的 做判斷 if (/[a-zA-Z$\_]/.test(currentChar)) { var token = { type: "identifier", value: currentChar }; tokens.push(token); // 繼續(xù)遍歷下一個(gè)字符,如果下一個(gè)字符還是以字母,$,_開始的話 for (i++; i < code.length; i++) { currentChar = code.charAt(i); if (/[a-zA-Z0-9$\_]/.test(currentChar)) { token.value += currentChar; } else { i--; break; } } continue; } // 連續(xù)的空白字符組合在一起 if (/s/.test(currentChar)) { var token = { type: "whitespace", value: currentChar } tokens.push(token); // 繼續(xù)遍歷下一個(gè)字符 for (i++; i < code.length; i++) { currentChar = code.charAt(i); if (/s/.test(currentChar)) { token.value += currentChar; } else { i--; break; } } continue; } // 更多的字符判斷 ...... // 遇到無法理解的字符 直接拋出異常 throw new Error("Unexpected " + currentChar); } return tokens; } var tokens = tokenizeCode(` if (1 > 0) { alert("aa"); } `); console.log(tokens);
測(cè)試一下:
解析結(jié)果如下:
0: {type: "whitespace", value: "? "} 1: {type: "identifier", value: "if"} 2: {type: "whitespace", value: " "} 3: {type: "parens", value: "("} 4: {type: "number", value: "1"} 5: {type: "whitespace", value: " "} 6: {type: "operator", value: ">"} 7: {type: "whitespace", value: " "} 8: {type: "number", value: "0"} 9: {type: "parens", value: ")"} 10: {type: "whitespace", value: " "} 11: {type: "brace", value: "{"} 12: {type: "whitespace", value: "? "} 13: {type: "identifier", value: "alert"} 14: {type: "parens", value: "("} 15: {type: "string", value: ""aa""} 16: {type: "parens", value: ")"} 17: {type: "sep", value: ";"} 18: {type: "whitespace", value: "? "} 19: {type: "brace", value: "}"} 20: {type: "whitespace", value: "? "}
有了詞法分析得出來的內(nèi)容下一步就是要把他們語義化,也就是知道他們代表的是什么意思,有什么聯(lián)系?比如說括號(hào)的范圍是什么?變量之間的關(guān)系是什么?具體看代碼,先寫一下大概的結(jié)構(gòu):
var parser = function(tokens){ const ast = { type:"Program", body:[] }; // 逐條解析頂層語句 while (i < tokens.length) { const statement = nextStatement(); if (!statement) { break; } ast.body.push(statement); } return ast; } var ast = parse([ {type: "whitespace", value: " "}, {type: "identifier", value: "if"}, {type: "whitespace", value: " "}, {type: "parens", value: "("}, {type: "number", value: "1"}, {type: "whitespace", value: " "}, {type: "operator", value: ">"}, {type: "whitespace", value: " "}, {type: "number", value: "0"}, {type: "parens", value: ")"}, {type: "whitespace", value: " "}, {type: "brace", value: "{"}, {type: "whitespace", value: " "}, {type: "identifier", value: "alert"}, {type: "parens", value: "("}, {type: "string", value: ""aa""}, {type: "parens", value: ")"}, {type: "sep", value: ";"}, {type: "whitespace", value: " "}, {type: "brace", value: "}"}, {type: "whitespace", value: " "} ]);
具體解析過程,生成ast樹:
var parse = function(tokens) { let i = -1; // 用于標(biāo)識(shí)當(dāng)前遍歷位置 let curToken; // 用于記錄當(dāng)前符號(hào) // 讀取下一個(gè)語句 function nextStatement () { // 暫存當(dāng)前的i,如果無法找到符合條件的情況會(huì)需要回到這里 stash(); // 讀取下一個(gè)符號(hào) nextToken(); if (curToken.type === "identifier" && curToken.value === "if") { // 解析 if 語句 const statement = { type: "IfStatement", }; // if 后面必須緊跟著 ( nextToken(); if (curToken.type !== "parens" || curToken.value !== "(") { throw new Error("Expected ( after if"); } // 后續(xù)的一個(gè)表達(dá)式是 if 的判斷條件 statement.test = nextExpression(); // 判斷條件之后必須是 ) nextToken(); if (curToken.type !== "parens" || curToken.value !== ")") { throw new Error("Expected ) after if test expression"); } // 下一個(gè)語句是 if 成立時(shí)執(zhí)行的語句 statement.consequent = nextStatement(); // 如果下一個(gè)符號(hào)是 else 就說明還存在 if 不成立時(shí)的邏輯 if (curToken === "identifier" && curToken.value === "else") { statement.alternative = nextStatement(); } else { statement.alternative = null; } commit(); return statement; } if (curToken.type === "brace" && curToken.value === "{") { // 以 { 開頭表示是個(gè)代碼塊,我們暫不考慮JSON語法的存在 const statement = { type: "BlockStatement", body: [], }; while (i < tokens.length) { // 檢查下一個(gè)符號(hào)是不是 } stash(); nextToken(); if (curToken.type === "brace" && curToken.value === "}") { // } 表示代碼塊的結(jié)尾 commit(); break; } // 還原到原來的位置,并將解析的下一個(gè)語句加到body rewind(); statement.body.push(nextStatement()); } // 代碼塊語句解析完畢,返回結(jié)果 commit(); return statement; } // 沒有找到特別的語句標(biāo)志,回到語句開頭 rewind(); // 嘗試解析單表達(dá)式語句 const statement = { type: "ExpressionStatement", expression: nextExpression(), }; if (statement.expression) { nextToken(); if (curToken.type !== "EOF" && curToken.type !== "sep") { throw new Error("Missing ; at end of expression"); } return statement; } } // 讀取下一個(gè)表達(dá)式 function nextExpression () { nextToken(); if (curToken.type === "identifier") { const identifier = { type: "Identifier", name: curToken.value, }; stash(); nextToken(); if (curToken.type === "parens" && curToken.value === "(") { // 如果一個(gè)標(biāo)識(shí)符后面緊跟著 ( ,說明是個(gè)函數(shù)調(diào)用表達(dá)式 const expr = { type: "CallExpression", caller: identifier, arguments: [], }; stash(); nextToken(); if (curToken.type === "parens" && curToken.value === ")") { // 如果下一個(gè)符合直接就是 ) ,說明沒有參數(shù) commit(); } else { // 讀取函數(shù)調(diào)用參數(shù) rewind(); while (i < tokens.length) { // 將下一個(gè)表達(dá)式加到arguments當(dāng)中 expr.arguments.push(nextExpression()); nextToken(); // 遇到 ) 結(jié)束 if (curToken.type === "parens" && curToken.value === ")") { break; } // 參數(shù)間必須以 , 相間隔 if (curToken.type !== "comma" && curToken.value !== ",") { throw new Error("Expected , between arguments"); } } } commit(); return expr; } rewind(); return identifier; } if (curToken.type === "number" || curToken.type === "string") { // 數(shù)字或字符串,說明此處是個(gè)常量表達(dá)式 const literal = { type: "Literal", value: eval(curToken.value), }; // 但如果下一個(gè)符號(hào)是運(yùn)算符,那么這就是個(gè)雙元運(yùn)算表達(dá)式 stash(); nextToken(); if (curToken.type === "operator") { commit(); return { type: "BinaryExpression", left: literal, right: nextExpression(), }; } rewind(); return literal; } if (curToken.type !== "EOF") { throw new Error("Unexpected token " + curToken.value); } } // 往后移動(dòng)讀取指針,自動(dòng)跳過空白 function nextToken () { do { i++; curToken = tokens[i] || { type: "EOF" }; } while (curToken.type === "whitespace"); } // 位置暫存棧,用于支持很多時(shí)候需要返回到某個(gè)之前的位置 const stashStack = []; function stash () { // 暫存當(dāng)前位置 stashStack.push(i); } function rewind () { // 解析失敗,回到上一個(gè)暫存的位置 i = stashStack.pop(); curToken = tokens[i]; } function commit () { // 解析成功,不需要再返回 stashStack.pop(); } const ast = { type: "Program", body: [], }; // 逐條解析頂層語句 while (i < tokens.length) { const statement = nextStatement(); if (!statement) { break; } ast.body.push(statement); } return ast; };
測(cè)試一下:
解析出來的ast的具體結(jié)構(gòu)如下:
{ "type": "Program", "body": [ { "type": "IfStatement", "test": { "type": "BinaryExpression", "left": { "type": "Literal", "value": 1 }, "right": { "type": "Literal", "value": 0 } }, "consequent": { "type": "BlockStatement", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "caller": { "type": "Identifier", "value": "alert" }, "arguments": [ { "type": "Literal", "value": "aa" } ] } } ] }, "alternative": null } ] }
至此生成ast樹,這樣就有了代碼的相關(guān)的信息,下一步就是把這些信息轉(zhuǎn)化成執(zhí)行代碼,這就是遍歷ast,然后eval處理就行了,具體看代碼:
const types = { Program (node) { var code = node.body.map(child => { return generate(child) }); // console.log(code) return code; }, IfStatement (node) { let code = `if ( ${generate(node.test)} ) { ${generate(node.consequent)} } `; if (node.alternative) { code += `else ${generate(node.alternative)}`; } return code; }, BinaryExpression(node){ let code = `${generate(node.left)} > ${generate(node.right)} `; return code; }, Literal (node) { let code = node.value; return code; }, BlockStatement(node){ let code = node.body.map(child => { return generate(child) }); return code; }, ExpressionStatement(node){ let code = `${generate(node.expression)}`; return code; }, CallExpression(node){ let alert = `${generate(node.caller)}`; let value = generate(node.arguments[0]); return `${alert}("${value}")`; }, Identifier(node){ let code = node.value; return code; } }; function generate(ast) { return types[ast.type](ast).toString(); } var code = generate({ "type": "Program", "body": [ { "type": "IfStatement", "test": { "type": "BinaryExpression", "left": { "type": "Literal", "value": 1 }, "right": { "type": "Literal", "value": 0 } }, "consequent": { "type": "BlockStatement", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "caller": { "type": "Identifier", "value": "alert" }, "arguments": [ { "type": "Literal", "value": "aa" } ] } } ] }, "alternative": null } ] }); // console.log(code) eval(code)
把代碼放在控制臺(tái)試試:
至此,通過解析一句話了解了js到底是如何處理代碼的。
2.閉包 2.1閉包基礎(chǔ)接著來看閉包,閉包是函數(shù)內(nèi)的函數(shù),實(shí)際是作用域內(nèi)的作用域,在js中為什么會(huì)出現(xiàn)閉包這個(gè)概念呢?是因?yàn)閖s中只有局部變量和全局變量,局部變量放在函數(shù)作用域內(nèi),全局變量放在全局作用域內(nèi),局部可以訪問全局但全局無法訪問局部,如果想要讓全局能夠訪問到局部,就需要通過閉包來實(shí)現(xiàn),具體看代碼:
function f(){ var a=1; } console.log(a)
`此時(shí)console處于全局,a處于局部,全局無法訪問局部,所以必然會(huì)報(bào)錯(cuò):
`
這時(shí)可以利用閉包來進(jìn)行解決,具體看代碼:
function f(){ var a=1; function g(){ console.log(a) }; return g; } f()()
此時(shí)就能夠訪問到局部的變量了,分析一下,我在f()中寫了g(),g()屬于f(),所以能訪問a,然后在外層把f返回,此時(shí)的f實(shí)際就是需要訪問的變量,看到這里有點(diǎn)疑惑,直接把a(bǔ)進(jìn)行return不也能達(dá)到這樣的目的么,為什么還要加一層?這個(gè)問題可以用一個(gè)例子來解釋,假設(shè)a是一個(gè)局部變量,但有需要被訪問到,同時(shí)還不希望所有的人都訪問到,那使用閉包包裝過的變量,只有知道包裝形式的人才能使用它,這個(gè)就是為什么需要多保障一層的原因
2.1閉包應(yīng)用 2.1.1 保護(hù)私有變量以上就是對(duì)閉包的解釋,來想一想閉包幫助我們擁有了訪問局部變量的能力,那它怎么用呢?
首先就是用于保護(hù)私有變量,導(dǎo)出公有變量,以jquery源碼入口結(jié)構(gòu)來進(jìn)行說明:
( function( global, factory ) { "use strict"; if ( typeof module === "object" && typeof module.exports === "object" ) { module.exports = global.document ? factory( global, true ) : function( w ) { if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { //具體代碼 return jQuery; }
把這個(gè)結(jié)構(gòu)抽離出來,如下:
( function() { }) ( )
第一個(gè)括號(hào)有兩個(gè)作用:
讓js解析器把后面的function當(dāng)作函數(shù)表達(dá)式而不是函數(shù)定義
形成一個(gè)作用域,類似在上面閉包例子中的f函數(shù)
第二個(gè)括號(hào)
觸發(fā)函數(shù)并傳參
2.1.2 定時(shí)器接著是定時(shí)器相關(guān)的應(yīng)用:
for( var i = 0; i < 5; i++ ) { setTimeout(() => { console.log( i ); }, 1000 * i) }
這個(gè)代碼的本意是要每隔1秒輸出01234,但實(shí)際上它會(huì)每隔1秒輸出5,因?yàn)閒or循環(huán)會(huì)很快執(zhí)行完,i的值固定為5,但setTimeout是異步操作會(huì)被掛起,等到異步操作完成的時(shí)候,i已經(jīng)是5,所以會(huì)輸出5,利用閉包來改造一下:
for( var i = 0; i < 5; i++ ) { ((j) => { setTimeout(() => { console.log( j ); }, 1000 * j) })(i) }
setTimeout的父級(jí)作用域自執(zhí)行函數(shù)中的j的值就會(huì)被記錄,實(shí)現(xiàn)目標(biāo)
2.1.3 DOM綁定事件再來看個(gè)例子,如果需要給頁面上多個(gè)div綁定點(diǎn)擊事件時(shí),一般是這樣寫:
test abc
但這樣寫會(huì)導(dǎo)致alert()的內(nèi)容都是c,原因和上面差不多,所以需要保存每次循環(huán)的內(nèi)容,所以可以這樣來寫:
2.2 內(nèi)存泄露test abc
上面說了什么是閉包以及閉包怎么用,那閉包會(huì)不會(huì)帶來一些不好的影響呢?
答案是內(nèi)存泄露,意思就是變量不被使用但還占用空間未被清除,對(duì)于局部的變量,它的生命周期是局部作用域被調(diào)用開始---局部作用域被調(diào)用完成,對(duì)于全局的變量,它的生命周期是整個(gè)應(yīng)用結(jié)束,比如關(guān)閉瀏覽器,函數(shù)中的變量毫無疑問是局部變量,但是由于使用了閉包,所以它被全局的某處使用,導(dǎo)致js的垃圾回收機(jī)制并不會(huì)將它回收,在不注意的情況下就會(huì)造成內(nèi)存泄露,在js中有兩種垃圾回收的方法:
一種是標(biāo)記回收,當(dāng)局部作用域生效開始,就會(huì)把局部作用域的變量進(jìn)行標(biāo)記,等到局部作用域失效時(shí),被標(biāo)記的內(nèi)容就會(huì)被清除
一種是引用回收,當(dāng)進(jìn)入作用域時(shí),會(huì)在變量上添加引用計(jì)數(shù),當(dāng)同一個(gè)值被賦給另一個(gè)變量時(shí)計(jì)數(shù)加1,當(dāng)該變量值修改時(shí)計(jì)數(shù)減1,如果在回收周期到來時(shí),計(jì)數(shù)為0,則會(huì)被回收
js本身實(shí)現(xiàn)了垃圾自動(dòng)回收,但是系統(tǒng)實(shí)際分配給瀏覽器的內(nèi)存總量是有限的,如果因?yàn)殚]包導(dǎo)致垃圾變量不被回收就會(huì)導(dǎo)致崩潰,具體看代碼:
function f1(){ var n=999; nAdd=function(){n+=1} function f2(){ alert(n); } return f2; } var result=f1(); result(); // 999 nAdd(); result(); // 1000
result實(shí)際上就是閉包f2函數(shù)。它一共運(yùn)行了兩次,第一次的值是999,第二次的值是1000。這證明,函數(shù)f1中的局部變量n一直保存在內(nèi)存中,并沒有在f1調(diào)用后被自動(dòng)清除,原因就在于f1是f2的父函數(shù),而f2被賦給了一個(gè)全局變量,這導(dǎo)致f2始終在內(nèi)存中,而f2的存在依賴于f1,因此f1也始終在內(nèi)存中,不會(huì)在調(diào)用結(jié)束后,被垃圾回收機(jī)制(garbage collection)回收,那怎么解決呢? result = null;手動(dòng)解除占用
function f1(){ var n=999; nAdd=function(){n+=1} function f2(){ alert(n); } return f2; } var result=f1(); result(); // 999 result = null; nAdd(); result(); // 10003.類 3.1原生實(shí)現(xiàn)類功能
在js語言中原本是沒有類這個(gè)概念的,但是隨著業(yè)務(wù)復(fù)雜又需要編寫面向?qū)ο蟮拇a,那怎么辦呢?創(chuàng)造一下,所以js就有了構(gòu)造函數(shù),原型對(duì)象,作用域鏈等一系列的概念,在其他高級(jí)語言中,類就是模板,具有對(duì)應(yīng)的屬性和方法,并且支持公有、私有,靜態(tài)屬性和方法等,一個(gè)類還必須滿足封裝繼承和多態(tài)三大性質(zhì),這樣的話根據(jù)類就能就能創(chuàng)造出實(shí)例對(duì)象了.
在js中默認(rèn)存在nativeobject(Function,Date..),built-in object(Global/Math)和host object(DOM/BOM),這些實(shí)例對(duì)象直接就能使用,但js的強(qiáng)大在于自己定制的類和實(shí)例化的對(duì)象,所以這就是接下來寫文的目的,如果自己來創(chuàng)造類的功能,你會(huì)怎么來做呢?
最開始想到的方法是Object,可以有屬性和方法,能否用它來實(shí)現(xiàn)?來試一試:
function showColor() { alert(this.color); } function createCar() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = showColor; return oTempCar; } var oCar1 = createCar(); var oCar2 = createCar();
注意到上面除了object還用了一個(gè)function createCar,其實(shí)這是創(chuàng)建類的一種設(shè)計(jì)模式,叫做工廠模式,避免了重復(fù)去new object,同時(shí)內(nèi)部的方法以屬性的形式來進(jìn)行關(guān)聯(lián),避免了每次調(diào)用工廠函數(shù)的時(shí)候重復(fù)生成對(duì)應(yīng)的方法
3.2構(gòu)造+原型實(shí)現(xiàn)類功能會(huì)發(fā)現(xiàn)雖然上述的工廠函數(shù)實(shí)現(xiàn)了屬性和方法的功能,但是屬性和方法是分離開的啊,有沒有辦法解決呢?用構(gòu)造函數(shù)+原型對(duì)象,構(gòu)造函數(shù)本質(zhì)上就是一個(gè)首字母大寫的函數(shù),只不過調(diào)用的時(shí)候是用new關(guān)鍵字來進(jìn)行生成,原型對(duì)象是為了解決類的方法重復(fù)創(chuàng)建的問題,所以將方法保存在原型對(duì)象中,然后在調(diào)用時(shí)沿著作用域鏈去尋找,那如何把方法綁定在原型對(duì)象上呢?每個(gè)構(gòu)造函數(shù)都可以通過prototype找到原型對(duì)象,具體看代碼,
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); } Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25); oCar1.drivers.push("Bill"); alert(oCar1.drivers); //輸出 "Mike,John,Bill" alert(oCar2.drivers); //輸出 "Mike,John"
會(huì)發(fā)現(xiàn)在構(gòu)造函數(shù)內(nèi)沒有創(chuàng)建對(duì)象,而是使用 this 關(guān)鍵字,新建實(shí)例時(shí)使用new 運(yùn)算符,那他們都干了啥?
1.new先新建了個(gè)空對(duì)象,就像在剛才的工廠函數(shù)中new Object()一樣,怎么證明呢?在控制臺(tái)測(cè)試一下
var Test = function(){} console.log(typeof Test)//function var test = new Test() console.log(typeof test)//object
會(huì)發(fā)現(xiàn)經(jīng)過new后test的類型變成了object
2.接著Car.__proto__=car.prototype,將實(shí)例的原型對(duì)象指向構(gòu)造函數(shù)的原型對(duì)象,為什么這么做呢,因?yàn)樵诠S函數(shù)中我們給對(duì)象添加方法是直接通過oTempCar.showColor = showColor;,但通過構(gòu)造+原型的方式來進(jìn)行添加函數(shù)時(shí),函數(shù)是被放在構(gòu)造函數(shù)的原型對(duì)象里的,這是為了在調(diào)用時(shí)避免重復(fù)生成方法,所以實(shí)例對(duì)象要想訪問到構(gòu)造函數(shù)的方法,就必須要將自己的原型對(duì)象指向構(gòu)造函數(shù)的原型對(duì)象,此時(shí)就可以訪問到對(duì)應(yīng)的方法了
3.再接著car.call(Car),把this指向當(dāng)前的對(duì)象,這是因?yàn)樵谄胀ê铮瑃his指向的是全局,只有進(jìn)行修改后才能指向當(dāng)前對(duì)象,這樣的話就能像工廠函數(shù)那樣的進(jìn)行屬性賦值了
這樣說太抽象,做了張圖大家看看:
前面曾經(jīng)說過this需要進(jìn)行綁定,因?yàn)樵诓煌淖饔糜蛳聇his所指代的內(nèi)容是不同的,所以在這里看一下this到底會(huì)指向什么東西
首先是全局作用域下的this:
console.log(this) //Window?{postMessage: ?, blur: ?, focus: ?, close: ?, parent: Window,?…}
接著是對(duì)象內(nèi)的this:
var obj = { user:"kk", a:function(){ console.log(this.user) }, b: { user: "gg", fn:function(){ console.log(this.user); } } } obj.a();//kk obj.b.fn();//gg
再來是函數(shù)內(nèi)的this:
var a = 1; function test(){ console.log(this.a) } test();//1
還有構(gòu)造函數(shù)中的this:
function Main(){ this.def = function(){ console.log(this === main); }; } Main.prototype.foo = function(){ console.log(this === main); } var main = new Main(); main.def(); //true main.foo();//true
得出了什么結(jié)論呢?this永遠(yuǎn)指向最后調(diào)用他的對(duì)象
3.3類的使用案例前面說了這么多的類的創(chuàng)建,實(shí)戰(zhàn)一下,看看學(xué)這么多到底有什么用?
1.字符串連接的性能,要先來知道一下,ECMAScript 的字符串是不可變的,要想對(duì)它做修改,必須經(jīng)過以下的幾個(gè)步驟:
var str = "hello "; str += "world";
創(chuàng)建存儲(chǔ) "hello " 的字符串。
創(chuàng)建存儲(chǔ) "world" 的字符串。
創(chuàng)建存儲(chǔ)連接結(jié)果的字符串。
把 str 的當(dāng)前內(nèi)容復(fù)制到結(jié)果中。
把 "world" 復(fù)制到結(jié)果中。
更新 str,使它指向結(jié)果。
如果代碼匯中只有幾次字符串拼接,那還沒什么影響,但如果有幾千次幾萬次呢,上面這些流程在每修改一次的時(shí)候就會(huì)執(zhí)行一遍,非常的耗費(fèi)性能,解決方法是用 Array 對(duì)象存儲(chǔ)字符串,然后用 join() 方法(參數(shù)是空字符串)創(chuàng)建最后的字符串,把它直接封裝成類來使用:
function StringBuffer () { this._strings_ = new Array(); } StringBuffer.prototype.append = function(str) { this._strings_.push(str); }; StringBuffer.prototype.toString = function() { return this._strings_.join(""); };
封裝好了,可以來對(duì)比一下傳統(tǒng)的字符串拼接和我們封裝的這種類之間的性能差異:
下面是兩者進(jìn)行1百萬次操作的耗時(shí)對(duì)比
Concatenation with plus: 568 milliseconds Concatenation with StringBuffer: 388 milliseconds3.4對(duì)象冒充繼承
上面已經(jīng)實(shí)現(xiàn)了js中類的創(chuàng)建,下一步要解決是類的繼承,最常用的有對(duì)象冒充繼承,原型鏈繼承和混合繼承
首先說對(duì)象冒充繼承,本質(zhì)就是把父類作為子類的一個(gè)方法,然后來調(diào)用它,具體看代碼:
function ClassA(sColor) { this.color = sColor; this.sayColor = function () { alert(this.color); }; } function ClassB(sColor, sName) { this.newMethod = ClassA; this.newMethod(sColor); delete this.newMethod; this.name = sName; this.sayName = function () { alert(this.name); }; } var objA = new ClassA("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //輸出 "blue" objB.sayColor(); //輸出 "red" objB.sayName(); //輸出 "John"
父類作為子類的一個(gè)方法時(shí)當(dāng)調(diào)用這個(gè)方法實(shí)際上父類的屬性和方法就被子類繼承了,同時(shí)我們還會(huì)發(fā)現(xiàn)delete this.newMethod;這句話,這是避免子類中新拓展的屬性或者方法覆蓋掉父類的屬性方法,經(jīng)過這樣的冒用,就實(shí)現(xiàn)了子類的繼承,同時(shí)這種方法還可以實(shí)現(xiàn)多重繼承,也就是一個(gè)子類繼承多個(gè)父類,da但是,這樣繼承的父類中若果有重復(fù)的屬性或者方法,會(huì)按照繼承順序來確定優(yōu)先級(jí),后繼承的優(yōu)先級(jí)高,具體看代碼:
function ClassZ() { this.newMethod = ClassX; this.newMethod(); delete this.newMethod; this.newMethod = ClassY; this.newMethod(); delete this.newMethod; }
這種繼承方法非常的流行,以至于官方后來擴(kuò)展了call()和apply()來簡(jiǎn)化上面的操作,call()第一個(gè)參數(shù)就是子類,第二個(gè)參數(shù)就是需要傳遞的參數(shù)[字符串],而apply()和call()的區(qū)別是,apply接受的參數(shù)形式為數(shù)組
//call function ClassA(sColor) { this.color = sColor; this.sayColor = function () { alert(this.color); }; } function ClassB(sColor, sName) { ClassA.call(this, sColor); this.name = sName; this.sayName = function () { alert(this.name); }; }
//apply function ClassA(sColor) { this.color = sColor; this.sayColor = function () { alert(this.color); }; } function ClassB(sColor, sName) { ClassA.apply(this, new Array(sColor)); this.name = sName; this.sayName = function () { alert(this.name); }; }
做了張圖,大家看看:
除了對(duì)象冒充繼承,還可以使用原型鏈繼承,原理是原型鏈最終會(huì)指向原型對(duì)象,換句話說,原型對(duì)象上的屬性方法能被對(duì)象實(shí)例訪問到,利用這個(gè)特性就可以實(shí)現(xiàn)繼承,怎么做呢?ClassB.prototype = new ClassA();搞定,但要記住,子類的所有新屬性和方法必須寫在這句話后面,因?yàn)榇藭r(shí)子類的原型對(duì)象實(shí)際上已經(jīng)是A的實(shí)例所指向的原型對(duì)象,如果寫在這句話前面,那新屬性和方法就被掛載到了B的原型對(duì)象上去了,經(jīng)過這句話賦值,那掛載的內(nèi)容就相當(dāng)于全被刪了,切記切記,還有一點(diǎn)要知道,原型鏈繼承并不能實(shí)現(xiàn)多重繼承,這是因?yàn)樵蛯?duì)象只有一個(gè),采用A的就不能用B的,否則就相當(dāng)于把前一個(gè)刪了。
function ClassA() { } ClassA.prototype.color = "blue"; ClassA.prototype.sayColor = function () { alert(this.color); }; function ClassB() { } ClassB.prototype = new ClassA(); ClassB.prototype.name = ""; ClassB.prototype.sayName = function () { alert(this.name); }; var objA = new ClassA(); var objB = new ClassB(); objA.color = "blue"; objB.color = "red"; objB.name = "John"; objA.sayColor(); objB.sayColor(); objB.sayName();
ClassB.prototype = new ClassA();是最重要的,它將ClassB 的 prototype 屬性設(shè)置成 ClassA 的實(shí)例,獲得了ClassA 的所有屬性和方法
3.6混合繼承對(duì)象冒充的主要問題是必須使用構(gòu)造函數(shù)方式,使用原型鏈,就無法使用帶參數(shù)的構(gòu)造函數(shù)了,所以可以將兩者結(jié)合起來:
function ClassA(sColor) { this.color = sColor; } ClassA.prototype.sayColor = function () { alert(this.color); }; function ClassB(sColor, sName) { ClassA.call(this, sColor); this.name = sName; } ClassB.prototype = new ClassA(); ClassB.prototype.sayName = function () { alert(this.name); }; var objA = new ClassA("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //輸出 "blue" objB.sayColor(); //輸出 "red" objB.sayName(); //輸出 "John"3.7多態(tài)
一個(gè)預(yù)語言能使用類這個(gè)功能,說明它至少滿足了類的三個(gè)特點(diǎn),封裝,繼承和多態(tài),前面說過了封裝和繼承,現(xiàn)在來說一下多態(tài),多態(tài):同一操作作用于不同的對(duì)象,可以有不同的解釋,產(chǎn)生不同的執(zhí)行結(jié)果??戳艘院蟾杏X很抽象,老辦法,舉例子,某人家里養(yǎng)了一只雞,一只鴨,當(dāng)主人向他們發(fā)出‘叫’的命令時(shí)。鴨子會(huì)嘎嘎的叫,而雞會(huì)咯咯的叫,轉(zhuǎn)換成代碼如下:
var makeSound = function(animal) { animal.sound(); } var Duck = function(){} Duck.prototype.sound = function() { console.log("嘎嘎嘎") } var Chiken = function() {}; Chiken.prototype.sound = function() { console.log("咯咯咯") } makeSound(new Chicken()); makeSound(new Duck());
JavaScript中大多是通過子類重寫父類方法的方式實(shí)現(xiàn)多態(tài),具體看代碼:
//使用es6 class簡(jiǎn)化代碼 class Parent { sayName() { console.log("Parent"); } } class Child extends Parent{ sayName() { console.log("Child"); } } function sayAge(object) { if ( object instanceof Child ){ console.log( "10" ); }else if ( object instanceof Parent ){ console.log( "30" ); } } sayAge(child); // "10" sayAge(parent); // "30"
很好玩,通過相同的操作但卻得到了不同的結(jié)果,這個(gè)就是多態(tài),這里以后再深入學(xué)習(xí)后會(huì)再補(bǔ)充的,留坑
3.8私有/靜態(tài)屬性和方法我們前面寫的類的屬性和方法都是公有的,但其實(shí)一個(gè)真正的類是包含只提供內(nèi)部使用的私有屬性方法和只提供類本身使用的靜態(tài)屬性和方法,接下來就一一實(shí)現(xiàn)一下:
首先是靜態(tài)屬性和方法,這個(gè)實(shí)現(xiàn)很簡(jiǎn)單,直接在類中添加就好了
function Person(name) { } //添加靜態(tài)屬性 Person.mouth = 1; //添加靜態(tài)方法 Person.cry = function() { alert("Wa wa wa …"); }; var me = new Person("Zhangsan"); me.cry(); //Uncaught TypeError: me.cry is not a function
接著是私有屬性和方法,其中私有方法又叫特權(quán)方法,它既可以訪問共有變量又可以訪問私有變量:
function Person(name) { //公有變量 this.name = name; //私有變量 let privateValue = 1; //私有方法 let privateFunc = function(){ console.log(this.name,privateValue) }; privateFunc() } console.log(new Persion("kk"))3.9ES6類的創(chuàng)建繼承
前面說了這么多才把js的類實(shí)現(xiàn)好,但每次寫代碼都要這么麻煩么?幸好ed6中已經(jīng)將剛才所說的內(nèi)容封裝好了,也就是常說的class和extends,大家叫他們是語法糖,實(shí)際原理就是上面講的內(nèi)容,那來看看到底怎么用es6來實(shí)現(xiàn)類的創(chuàng)建與繼承
首先是創(chuàng)建:
class Animal{ constructor(name){ this.name = name; }; sayNmae(){ console.log(this.name) } } let animal = new Animal("小狗"); console.log(animal.name); animal.sayNmae("小汪")
會(huì)發(fā)現(xiàn)多了一些關(guān)鍵字class和constructor,并且方法也寫在了類里面,其中class和原來的function對(duì)比來看,說明在使用時(shí)只能有new這一種調(diào)用方式,而不是像以前一樣技能當(dāng)構(gòu)造函數(shù)又能當(dāng)普通函數(shù),constructor和原來的this差不多都是指向了當(dāng)前的對(duì)象做完了就把對(duì)象返回
接著是繼承:
class Dog extends Animal{ constructor(name,type){ super(name); this.type = type } sound(content){ console.log(content); } } let dog = new Dog("小狗","aaa"); console.log(dog.name) dog.sayNmae() console.log(dog.type) dog.sound("汪汪汪")
同樣發(fā)現(xiàn)多了一些關(guān)鍵字extends和super(),其中extends相當(dāng)于原來的Parent.apply(this),super相當(dāng)于原來的ClassB.prototype = new ClassA();,也就是指向存放屬性和方法的原型對(duì)象
ok,至此,關(guān)于類的內(nèi)容告一段落,其實(shí)還有很多內(nèi)容可以說,比如設(shè)計(jì)模式,但它包含的內(nèi)容太多了,以后多帶帶開一篇來說。
js是單線程語言,所以出現(xiàn)了耗時(shí)的操作時(shí)候,腳本會(huì)被卡死,這就需要處理異步的操作的機(jī)制,在最開始,js處理異步的方法是采用回調(diào)函數(shù),比如下面這個(gè)例子:
function test(){ setTimeout(() => { console.log("a") },2000) } test() console.log("b")
期望的結(jié)果是先a后b,但打印的結(jié)果是先b后a如何解決呢?
function test(f){ setTimeout(() => { console.log("a") f() },2000) } test(() => { console.log("b") })
確實(shí)達(dá)到了目的,但是如果需要嵌套的層數(shù)特別多的時(shí)候會(huì)導(dǎo)致地獄回調(diào),不利于代碼維護(hù),所以es6提出了promise來解決這個(gè)問題
function test(){ return new Promise((resolve,reject) => { setTimeout(() => { console.log("a"); resolve() },2000) }) } test() .then(() => { return new Promise((resolve,reject) => { setTimeout(()=> { let a = 1; if(a){ reject() }else{ console.log("b"); resolve(); } }) },1000) }) .then(() => { console.log("c") }).catch((err) => { console.log("error") })4.3async/await
通過這樣的方法確實(shí)實(shí)現(xiàn)了操作并且將邏輯拆開了避免了callback hell,但是這樣寫還是不舒服,看著很難受,所以可以用async、await來進(jìn)行書寫:
function a(){ setTimeout(() => { console.log("a") },2000) } function b(){ setTimeout(() => { console.log("b") },1000) } async function test(){ try { await a(); await b(); }catch(ex){ console.log("error") } } test()
ok,完美解決
首先說一下,為什么需要模塊化,在es6之前,如果有多個(gè)文件,文件彼此之間相互依賴,最簡(jiǎn)單的就是后一個(gè)文件要調(diào)用前一個(gè)文件的變量,怎么做呢?前一個(gè)文件就會(huì)將該變量綁定在window頂層對(duì)象上暴露出去,這樣做確實(shí)達(dá)到了目的,但是同時(shí)也帶來了新的問題,如果一個(gè)項(xiàng)目是多人開發(fā)的,其他人不知道你到底定義了什么內(nèi)容,很有可能會(huì)把原先你定義好的變量給覆蓋掉,這是第一個(gè)致命,的地方除此以外,當(dāng)自己寫了一個(gè)模塊,在導(dǎo)入的時(shí)候,有可能因?yàn)槟K文件過大導(dǎo)致加載速度很慢,這是第二個(gè)致命的地方,前面兩點(diǎn)在開發(fā)時(shí)定好開發(fā)的規(guī)范,盡量拆分模塊為單一的體積小的內(nèi)容還是可以解決的,但是還有一點(diǎn)就是模塊之間的加載順序,如果調(diào)用在前而加載在后,那肯定會(huì)報(bào)錯(cuò),這是第三個(gè)致命的地方,并且這種出錯(cuò)還不好排查
為了解決這些問題,先后有很多的模塊化規(guī)范被提出,那想想,一個(gè)良好的模塊應(yīng)該是什么樣的?總結(jié)了一下,應(yīng)該具有:
1.保證不與其他模塊發(fā)生變量名沖突
2.只暴露特定的模塊成員
3.模塊與模塊之間語義分明
4.支持異步加載
5.模塊加載順序不會(huì)影響調(diào)用
5.2瀏覽器模塊化AMD首先是AMD(Asynchronous Module Definition),它是專門為瀏覽器中JavaScript環(huán)境設(shè)計(jì)的規(guī)范,使用方法如下:
1.新建html引入requirejs并通過data-main="main.js"指定主模塊
//index.htmlrequirejs
2.接著在主模塊中加載需要用到的其他模塊,比如math.js,加載模塊固定使用require(),第一個(gè)參數(shù)是個(gè)數(shù)組指定加載的模塊,第二個(gè)是個(gè)回調(diào)函數(shù),當(dāng)加載完成后具體的執(zhí)行就在這里
//main.js require(["math"], function (math){ alert(math.foo()); });
3.被引用的模塊寫在define函數(shù)中,如果還有引用的模塊,就把第一個(gè)參數(shù)寫成數(shù)組來調(diào)用
//math.js define(["num"], function(num){ function foo(){ return num.number(); } return { foo : foo }; });
//num.js define(function (){ var number = function (){ var a = 5; return a; }; return { number: number }; });
好多自己以前寫的模塊并沒有使用define來定義,所以并不支持AMD的規(guī)范,那如何來加載這些內(nèi)容呢?可以通過require.config({ })來進(jìn)行加載:
//main.js require.config({ paths:{ "NotAmd":"./jutily" }, shim:{ "NotAmd":{ exports:"NotAmd" } } }); require(["math","NotAmd"], function (math){ alert(math.foo()); console.log(NotAmd()) });
//jutily.js (function(global) { global.NotAmd = function() { return "c, not amd module"; } })(window);5.3ES6模塊化
不管是AMD還是CMD,說到底它們都是加載的外來模塊實(shí)現(xiàn)js代碼的規(guī)范,但這樣寫也太麻煩了,于是es6中本身就開始支持模塊化了,具體如下:
//export.js let myName="laowang"; let myAge=90; let myfn=function(){ return "我是"+myName+"!今年"+myAge+"歲了" } export { myName, myAge, myfn } //export default= { myName, myAge, myfn }
import {myfn,myAge,myName} from "./export.js"; //import * as info from "./export.js"; console.log(myfn());//我是laowang!今年90歲了 console.log(myAge);//90 console.log(myName);//laowang
發(fā)現(xiàn)上面有個(gè)export和export default 兩者的區(qū)別是前者可以出現(xiàn)多次后者只能出現(xiàn)一次,可以混合出現(xiàn)這兩種導(dǎo)出方式
5.4commonjs模塊化前面的規(guī)范適用于瀏覽器端的js編程,但是現(xiàn)在的js早已經(jīng)不再局限在瀏覽器了,在服務(wù)端同樣也能使用,這就需要在服務(wù)端也實(shí)現(xiàn)js的模塊化,這就是commonjs,具體使用如下:
//export.js var x = 5; var addX = function (value) { return value + x; }; exports.x = x; module.exports.addX = addX;
var example = require("./example.js"); console.log(example.x); // 5 console.log(example.addX(1)); // 6
發(fā)現(xiàn)有exports和module.exports,他們的區(qū)別是什么呢?其實(shí)兩者差不多,但是如果要導(dǎo)出的是函數(shù)的時(shí)候就寫在module.exports上
重點(diǎn)要理解一下require的內(nèi)容,它的大概原理是:
檢查 Module._cache,是否緩存之中有指定模塊
緩存之中沒有,就創(chuàng)建一個(gè)新的Module實(shí)例
把它保存到緩存
使用 module.load() 加載指定的模塊文件,讀取文件內(nèi)容之后,使用 module.compile() 執(zhí)行文件代碼
如果加載/解析過程報(bào)錯(cuò),就從緩存刪除該模塊
返回該模塊的 module.exports
參考文章:
1.Babel是如何編譯JS代碼的及理解抽象語法樹(AST):https://www.cnblogs.com/tugen...
2.Babel是如何讀懂JS代碼的:
https://zhuanlan.zhihu.com/p/...
3.用 Chrome 開發(fā)者工具分析 javascript 的內(nèi)存回收(GC)
https://www.oschina.net/quest...
4.ECMAScript 定義類或?qū)ο?
http://www.w3school.com.cn/js...
5.ECMAScript 繼承機(jī)制實(shí)現(xiàn):
http://www.w3school.com.cn/js...
6.js 多態(tài)如何理解,最好能有個(gè)例子
https://segmentfault.com/q/10...
7.Javascript模塊化編程(一):模塊的寫法:
http://www.ruanyifeng.com/blo...
8.Javascript模塊化編程(二):AMD規(guī)范:
http://www.ruanyifeng.com/blo...
9.Javascript模塊化編程(三):require.js的用法
http://www.ruanyifeng.com/blo...
10.CommonJS規(guī)范
http://javascript.ruanyifeng....
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/109643.html
摘要:是當(dāng)時(shí)唯一的書,而且只有語言規(guī)范。仍然在中使用未來可能被取代,但不是現(xiàn)在。仍然是大學(xué)里教授的主要語言,并且存在于很多優(yōu)秀的庫中,比如。筆者期待積極的討論。的確存在缺陷,但這些缺陷并不妨礙它在世界上最主要的公司和系統(tǒng)內(nèi)全天候地完成工作。 【編者按】本文作者為資深碼農(nóng) Tim Spann,主要講述 Java 讓人無法抗拒的眾多優(yōu)點(diǎn)以及一些些缺陷。本文系國(guó)內(nèi) ITOM 管理平臺(tái) OneAPM...
摘要:提供一種可選的決策方案換一種思維看待決策能夠做的事情,也可以,反之也是,所以選擇它們很簡(jiǎn)單,如果公司前端多,就選擇,如果公司后端多,就選擇,當(dāng)然這只是個(gè)人觀點(diǎn)哈。 php和javascript都是非常流行的編程語言,剛剛開始一個(gè)服務(wù)于服務(wù)端,一個(gè)服務(wù)于前端,長(zhǎng)久以來,它們都能夠和睦相處,直到有一天,一個(gè)叫做node.js的JavaScript運(yùn)行環(huán)境誕生后,再加上PHP的swoole擴(kuò)...
摘要:本文建議有基礎(chǔ)的人看,由于內(nèi)容過多,所以建議配合高級(jí)程序設(shè)計(jì)服用。一共由三部分組成,分別是最新版本是,簡(jiǎn)稱,,。 本文建議有html基礎(chǔ)的人看,由于js內(nèi)容過多,所以建議配合《javascript高級(jí)程序設(shè)計(jì)》服用。 在開始前我先簡(jiǎn)單介紹一下javascript這門語言吧。 javascript誕生于1995年,主要是用來表單的驗(yàn)證,雖然名字里面有java,但是和java毫無關(guān)系,甚至...
摘要:本文建議有基礎(chǔ)的人看,由于內(nèi)容過多,所以建議配合高級(jí)程序設(shè)計(jì)服用。一共由三部分組成,分別是最新版本是,簡(jiǎn)稱,,。 本文建議有html基礎(chǔ)的人看,由于js內(nèi)容過多,所以建議配合《javascript高級(jí)程序設(shè)計(jì)》服用。 在開始前我先簡(jiǎn)單介紹一下javascript這門語言吧。 javascript誕生于1995年,主要是用來表單的驗(yàn)證,雖然名字里面有java,但是和java毫無關(guān)系,甚至...
摘要:構(gòu)造函數(shù)模式定義構(gòu)造函數(shù)模式是語言創(chuàng)建對(duì)象的通用方式。但兩種語言用構(gòu)造函數(shù)創(chuàng)建對(duì)象的方式略有不同在中沒有類的概念,函數(shù)即為一等公民,因此,不必顯式聲明某個(gè)類,直接創(chuàng)建構(gòu)造函數(shù)即可,類的方法和屬性在構(gòu)造函數(shù)中或原型對(duì)象上處理。 工廠模式 定義:工廠模式非常直觀,將創(chuàng)建對(duì)象的過程抽象為一個(gè)函數(shù),用函數(shù)封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié)。通俗地講,工廠模式就是將創(chuàng)建對(duì)象的語句放在一個(gè)函數(shù)里,通...
摘要:而對(duì)于二維數(shù)組,因?yàn)閮?nèi)存連續(xù)性的原因,內(nèi)存并不會(huì)真真的開辟一個(gè)二維空間,而是連續(xù)依次存入二維數(shù)組的每個(gè)數(shù)據(jù)。之所以有二維數(shù)組的說法是為了分析問題方便。二維數(shù)組的實(shí)質(zhì)是一維數(shù)組,只是其元素類型是一維數(shù)組類型。 ...
閱讀 3615·2021-11-15 11:38
閱讀 2812·2021-11-11 16:55
閱讀 2565·2021-11-08 13:22
閱讀 2640·2021-11-02 14:45
閱讀 1324·2021-09-28 09:35
閱讀 2605·2021-09-10 10:50
閱讀 475·2019-08-30 15:44
閱讀 2787·2019-08-29 17:06