JS語言缺陷

JS語言缺陷

js是一門在極短期裏創造的腳本語言,它存在不少的不足,這使得在學習時無形加大了學習的難度,本文就將這些內容進行總結,以防繼續掉坑。

1.變量提高

1.1 案例分析

先來講一下變量提高,它其實就是先用後聲明,常常被拿來講明的一個例子是:javascript

console.log(a);
var a = 10;//undefined

這是因爲這段代碼在執行的時候,js解析器會先把var聲明放在前面,而後順序執行對應的語句,執行到console的時候,因爲a變量已經聲明提高但未進行賦值操做,在js中這種狀況就會報undefined
上面是對出錯的解釋,接下來就細細說明一下變量提高的具體內容
先來講一下什麼是變量,變量就是存放數據的空間,在這個空間裏,能夠存放具體的數據,也能夠存放數據對應的地址,這其實是對應數據結構中的堆棧,棧數據少能夠直接將數據存放進來,堆數據多,因此另開空間存放,而後把數據對應的內存地址放在棧內,在賦值時,棧類型的數據會直接把數據拷貝一份而後進行賦值,而堆類型的數據會把地址複製一份,而後不一樣的變量會指向同一個地址,在js中對象,函數,數組等都是堆類型數據,也叫引用類型數據,下面直接在控制檯寫個小例子看看:css

//基本類型
//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被設置爲一等公民,也就是說,在聲明變量時函數變量的等級比其餘變量的等級高,函數的建立又有兩種方式一種是函數聲明,另外一種是函數表達式,在變量提高的時候,只會提高聲明而不會提高表達式:html

//函數聲明
function say(){
  console.log('saying');
}

//函數表達式
var say = function(){
  console.log('saying');
}

到這裏先來總結一下,爲了理解變量提高,先了解了變量是什麼,變量類型有哪些,函數建立的形式有什麼,接下來就可來檢驗一下,看是否真的懂了:java

var name = 'kk';

function say(){
  console.log(name); //輸出:undefined
  var name = 'zoe';
  console.log(name); //輸出:'zoe'
}

say();

來解釋一下爲何:node

//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'

再來看一個例子:jquery

var say = function(){
  console.log('1');
};

function say(){
  console.log('2');
};

say(); //輸出:'1'

說一下爲何:es6

//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樹,這樣說太抽象了,舉例看看:express

//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-執行,再次聲明,其餘全部可能存在的問題都不考慮,只是完成解析上面的一句話,開始:segmentfault

這句話對於計算機來講就是個字符串,那如何識別它呢?首先把這句話拆分,而後把拆分的內容組合,這個實際叫作詞法解析和語法組合,生成對應的類型和值,那這句話中有什麼?

1.'if' 
2.' ' 
3.'(' 
4.'1' 
5.' ' 
6.'>' 
7.' ' 
8.'0' 
9.')' 
10.' ' 
11.'{' 
12.'\n ' 
13.'alert' 
14.'(' 
15."aa" 
16.')' 
17.";" 
18.'\n' 
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);

測試一下:

ss

解析結果以下:

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: "\n"},
          {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: "\n"},
          {type: "identifier", value: "alert"},
          {type: "parens", value: "("},
          {type: "string", value: "'aa'"},
          {type: "parens", value: ")"},
          {type: "sep", value: ";"},
          {type: "whitespace", value: "\n"},
          {type: "brace", value: "}"},
          {type: "whitespace", value: "\n"}
                ]);

具體解析過程,生成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;
      };

測試一下:

sfgt

解析出來的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)

把代碼放在控制檯試試:

ssfrg

至此,經過解析一句話瞭解了js究竟是如何處理代碼的。

2.閉包

2.1閉包基礎

接着來看閉包,閉包是函數內的函數,實際是做用域內的做用域,在js中爲何會出現閉包這個概念呢?是由於js中只有局部變量和全局變量,局部變量放在函數做用域內,全局變量放在全局做用域內,局部能夠訪問全局但全局沒法訪問局部,若是想要讓全局可以訪問到局部,就須要經過閉包來實現,具體看代碼:

function f(){
  var a=1;
}
console.log(a)

`此時console處於全局,a處於局部,全局沒法訪問局部,因此必然會報錯:
`
tyjk

這時能夠利用閉包來進行解決,具體看代碼:

function f(){
  var a=1;
  function g(){
    console.log(a)
  };
  return g;
}
f()()

tfgy

此時就可以訪問到局部的變量了,分析一下,我在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() { }) ( )
  • 第一個括號有兩個做用:

    1. 讓js解析器把後面的function看成函數表達式而不是函數定義
    2. 造成一個做用域,相似在上面閉包例子中的f函數
  • 第二個括號

    1. 觸發函數並傳參

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綁定點擊事件時,通常是這樣寫:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <div>a</div>
    <div>b</div>
    <div>c</div>
    <script>
        function bindEvent(){
            var letters = ['A','B','C'];
            var elems = document.getElementsByTagName('div');
            var len=elems.length;
            for(var i=0; i<len; i++){
                var letter = letters[i];
                elems[i].addEventListener('click',function(){
                    alert(letter)
                })
            }
         }
        bindEvent()
    </script>
</body>
</html>

frgy

但這樣寫會致使alert()的內容都是c,緣由和上面差很少,因此須要保存每次循環的內容,因此能夠這樣來寫:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <div>a</div>
    <div>b</div>
    <div>c</div>
    <script>
        function createFunction(letter){
            return function(){
                alert(letter);
            }
        }
        function bindEvent(){
            var letters = ['A','B','C'];
            var elems = document.getElementsByTagName('div');
            var len=elems.length;
            for(var i=0; i<len; i++){
                var letter = letters[i];
                elems[i].onclick = createFunction(letter)
            }
         }
        bindEvent()
    </script>
</body>
</html>

frgy

2.2 內存泄露

上面說了什麼是閉包以及閉包怎麼用,那閉包會不會帶來一些很差的影響呢?
答案是內存泄露,意思就是變量不被使用但還佔用空間未被清除,對於局部的變量,它的生命週期是局部做用域被調用開始---局部做用域被調用完成,對於全局的變量,它的生命週期是整個應用結束,好比關閉瀏覽器,函數中的變量毫無疑問是局部變量,可是因爲使用了閉包,因此它被全局的某處使用,致使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(); // 1000

3.類

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指向的是全局,只有進行修改後才能指向當前對象,這樣的話就能像工廠函數那樣的進行屬性賦值了
這樣說太抽象,作了張圖你們看看:
dkkfi

前面曾經說過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("");
};

封裝好了,能夠來對比一下傳統的字符串拼接和咱們封裝的這種類之間的性能差別:

<html>
<body>

<script type="text/javascript">

function StringBuffer () {
  this._strings_ = new Array();
}

StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};

StringBuffer.prototype.toString = function() {
  return this._strings_.join("");
};

var d1 = new Date();
var str = "";
for (var i=0; i < 1000000; i++) {
    str += "text";
}
var d2 = new Date();

document.write("Concatenation with plus: "
 + (d2.getTime() - d1.getTime()) + " milliseconds");

var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 1000000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();

document.write("<br />Concatenation with StringBuffer: "
 + (d2.getTime() - d1.getTime()) + " milliseconds");

</script>

</body>
</html>

下面是二者進行1百萬次操做的耗時對比

Concatenation with plus: 568 milliseconds
Concatenation with StringBuffer: 388 milliseconds

3.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);
    };
}

作了張圖,你們看看:
xsdt

3.5原型鏈繼承

除了對象冒充繼承,還可使用原型鏈繼承,原理是原型鏈最終會指向原型對象,換句話說,原型對象上的屬性方法能被對象實例訪問到,利用這個特性就能夠實現繼承,怎麼作呢?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,至此,關於類的內容告一段落,其實還有不少內容能夠說,好比設計模式,但它包含的內容太多了,之後單獨開一篇來講。

4.異步

4.1回調函數

js是單線程語言,因此出現了耗時的操做時候,腳本會被卡死,這就須要處理異步的操做的機制,在最開始,js處理異步的方法是採用回調函數,好比下面這個例子:

function test(){
    setTimeout(() => {
        console.log('a')
    },2000)
}

test()
console.log('b')

jdjkg
指望的結果是先a後b,但打印的結果是先b後a如何解決呢?

function test(f){
    setTimeout(() => {
        console.log('a')
        f()
    },2000)
}

test(() => {
    console.log('b')
})

ksjf
確實達到了目的,可是若是須要嵌套的層數特別多的時候會致使地獄回調,不利於代碼維護,因此es6提出了promise來解決這個問題

4.2Promise

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')
        })

ksjf

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()

ksjf
ok,完美解決

5.模塊化

5.1原生模塊化

首先說一下,爲何須要模塊化,在es6以前,若是有多個文件,文件彼此之間相互依賴,最簡單的就是後一個文件要調用前一個文件的變量,怎麼作呢?前一個文件就會將該變量綁定在window頂層對象上暴露出去,這樣作確實達到了目的,可是同時也帶來了新的問題,若是一個項目是多人開發的,其餘人不知道你到底定義了什麼內容,頗有可能會把原先你定義好的變量給覆蓋掉,這是第一個致命,的地方除此之外,當本身寫了一個模塊,在導入的時候,有可能由於模塊文件過大致使加載速度很慢,這是第二個致命的地方,前面兩點在開發時定好開發的規範,儘可能拆分模塊爲單一的體積小的內容仍是能夠解決的,可是還有一點就是模塊之間的加載順序,若是調用在前而加載在後,那確定會報錯,這是第三個致命的地方,而且這種出錯還很差排查
爲了解決這些問題,前後有不少的模塊化規範被提出,那想一想,一個良好的模塊應該是什麼樣的?總結了一下,應該具備:

  • 1.保證不與其餘模塊發生變量名衝突
  • 2.只暴露特定的模塊成員
  • 3.模塊與模塊之間語義分明
  • 4.支持異步加載
  • 5.模塊加載順序不會影響調用

5.2瀏覽器模塊化AMD

首先是AMD(Asynchronous Module Definition),它是專門爲瀏覽器中JavaScript環境設計的規範,使用方法以下:
1.新建html引入requirejs並經過data-main="main.js"指定主模塊

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>requirejs</title>
</head>
<body>
    
    <script src="https://cdn.bootcss.com/require.js/2.3.6/require.js" defer async="true" data-main="main.js"></script>
    </script>
</body>
</html>

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....

相關文章
相關標籤/搜索