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