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

相关文章
相关标签/搜索