# 重学js

# 原型以及原型链

# 原型

本质是一个js对象(除了null),当我们通过构造函数创建实例的时候,构造函数的其中一个属性prototype指向原型实例_proto_指向原型

实例对象与原型对象之间通过_proto_来关联起来,当实例的属性找不到的时候会通过_proto继续往上层原型对象查找,直到找到Object.prototype为止

用于创建出来的实例共享属性以及方法

# 原型链

原型链是通过实例的_proto属性,查找原型对象,如果找不到,会往上查找。这样成为一个原型链,一般是用于继承

关于Function__proto__===Function.prototype的问题

从探究Function.proto===Function.prototype过程中的一些收获 (opens new window)

原型图

# 作用域

js是词法作用域,也就是静态作用域

静态作用域是,函数作用域在(函数定义)的时候就决定了 (该作用域是父级创建的所以作用域链)

动态作用域是,函数作用域在函数调用的时候才决定的。

# 个人理解作用域

作用域有两种

一种是函数创建的时候,会有个静态作用域,静态作用域是指将父级或父级以上的作用域的所有变量对象,可以理解成一个父级的作用域链

第二种,就是函数执行的时候,会创建一个执行上下文,也就是会创建一个作用域,是函数独有的作用域,两者加起来就是该函数整体的作用域

# 执行上下文

js引擎执行代码时会创建执行环境(执行上下文)

  • 创建全局函数

  • 函数执行

  • eval方法调用

# 生命周期

# 创建阶段

在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。

# 代码执行阶段

创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码

# 执行上下文都会包含三个重要属性

1.变量对象(variable Object VO)

2.作用域链(scoped chain)

3.this

# 变量对象(VO)

  1. 函数的所有形参(如果是函数上下文)

    • 名称和对应值组成的一个变量对象的属性被创建

    • 没有实参,为undefined

  2. 函数声明

    • 名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建

    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性

  3. 变量声明

    • 名称和对应值(undefined)组成一个变量对象的属性被创建;

    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

# 变量对象(VO)与活动对象(AO)区别

每次执行上下文之前,会保存上下文定义的属性以及函数声明, 该阶段是不可访问

执行阶段之后,变量对象(VO)会转成 活动对象(AO),里面的属性可以访问

本质上都是同一个对象,区别在于运行不同的对象周期

JavaScript深入之作用域链 (opens new window)

总结

  1. 全局上下文的变量对象初始化是全局对象 (window)

  2. 函数上下文的变量对象初始化只包括 Arguments 对象

  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

  4. 在代码执行阶段,会再次修改变量对象的属性值

# this

# 从ECMAScript规范

判断this

  1. 计算 MemberExpression 的结果赋值给 ref MemberExpression 左边表达式

  2. 判断 ref 是不是一个 Reference 类型。

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)

2.3 如果 ref 不是 Reference,那么 this 的值为 undefined

由于不是严格模式,this如果是undefined,会指向全局对象window

JavaScript深入之从ECMAScript规范解读this (opens new window)

# 常规理解

this就是最后调用他的函数,

  1. 直接调用 window (window 绑定)

  2. 对象调用指向对象 (隐式绑定)

  3. new 调用指向实例 (new 绑定)

  4. 可以通过call, apply, bind改变(显式绑定)

1. 查看函数在哪被调用。
2. 左侧有没有对象?如果有,它就是 “this” 的引用。如果没有,继续第 3 步。
3. 该函数是不是用 “call”、“apply” 或者 “bind” 调用的?如果是,它会显式地指明 “this” 的引用。如果不是,继续第 4 步。
4. 该函数是不是用 “new” 调用的?如果是,“this” 指向的就是 JavaScript 解释器新创建的对象。如果不是,继续第 5 步。
5. 是否在“严格模式”下?如果是,“this” 就是 undefined,如果不是,继续第 6 步。
6. JavaScript 很奇怪,“this” 会指向 “window” 对象。

# 闭包

指那些能够访问自由变量的函数

  1. 创建它的执行上下文被销毁了,但它仍然存在(比如内部函数从父函数返回过来)

  2. 在代码中引用了自由变量

因为闭包能访问以及销毁它但执行上下文访问到变量。而这个变量是无法被销毁的,存储在内存中。

# 参数按值传递

参数传递都是按值来传递的,但是js中有分基本类型和引用类型,所说的值传递是指栈中的值拷贝

基本类型是存储在栈内存中,但引用类型是将引用地址存在栈内存中,而数据存在堆内存中

# JavaScript 语言在引擎级别的执行过程

# 一、环境的准备

1.作用域 Scope

作用域本身有两个成员,objectparent,作用域中包含对象及属性

作用域主要有两项功能

  • 查找名字
  • 如果没有,查找 parent 上一层

2.环境 Environment

词法环境规范:环境记录和 outer,环境记录可以映射为作用域中的 objectouter 映射为作用域中的 parent

3.属性标识符

ES5的重要规范是属性描述符和属性标识符规范, 所有的环境记录对外统一用一个有意义的interface, 即标识符引用 GetIdentifierReference

标识符引用的所用是代替作用域查找名字的功能,统一格式

# 二. 可执行上下文 Executive Context

执行上下文添加了两个成员,词法环境变量环境

理论上词法环境变量环境只需要有一个就可以查找名字。但 JavaScript 中变量环境解决 var 声明,词法环境解决一般变量声明,两种声明在 JavaScript 中不兼容

1.代码层面如何 run

执行栈(ECS)为空时,会自动去找任务队列中的函数,并且执行,执行栈是先进后出的原则,任务队列是先进先出的原则

一开始,会执行

  1. 内核引擎所需要执行上下文(newContext for job),之后

  2. 通过newContext创建scriptContext执行上下文(变量环境和词法环境)

ScriptContext 执行上下文具体还可分为四种可执行的上下文,全局初始化模块初始化环境实例化函数环境实例化 Eval 环境等。

1.执行表达式

执行表达式返回的结果包括原始值,对象,引用规范类型。

2.执行语句

执行语句返回的结果是完整规范类型,表示语句是否被完整执行,是否中断,返 回值不包含引用。

# 继承

面向对象三大特性

继承

封装:低耦合高内聚

多态:重载和重写

  • 重载:方法名相同,形参的个数或者类型不一样(js不存在真正意义上的重载)(可以通过arguments)实现重载
  • 重写:类的继承中,子类可以重写继承父类的方法

# 原型链继承

原理:child.prototype = new Parent()

  1. 引用类型的属性会被所有实例共享

  2. 在创建实例的时候,不能向父级构造函数传参

  3. 不能实现多继承

# 构造函数继承

原理:Parent.call(this)

# 优点

  1. 避免引用类型属性被所有实例共享

  2. 创建实例的时候可以传入参数

  3. 多继承

# 缺点

方法都定义在构造器中,每次创建实例,会创建一遍方法

# 组合(原型链和构造函数)继承

融合原型链和构造函数都优点

缺点 调用两次父构造函数

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {

    Parent.call(this, name);
    this.age = age;

}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

# 原型式继承

缺点 引用类型属性共享在实例中,和原型链继承一样

Object.create
function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

# 寄生式继承

缺点 跟借用构造函数模式一样,每次创建对象都会创建一遍方法

function createObj (o) {
    var clone = Object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}

# 寄生组合式继承

它只调用了一次 Parent 构造函数

# 乱序

遍历元素,获取元素,再将剩余的个数随机获取。互换元素

function shuffle(a) {
    var j, x, i;
    for (i = a.length; i; i--) {
        j = Math.floor(Math.random() * i);
        x = a[i - 1];
        a[i - 1] = a[j];
        a[j] = x;
    }
    return a;
}

function shuffle (arr) {
  for (var i = 0; i<arr.length; i++) {
    var j = Math.floor(Math.random() * arr.length)

    var x
    x = arr[i]
    arr[i] = arr[j]
    arr[j] = x
  }
  return arr
}
function shuffle(a) {
    for (let i = a.length; i; i--) {
        let j = Math.floor(Math.random() * i);
        [a[i - 1], a[j]] = [a[j], a[i - 1]];
    }
    return a;
}

function shuffle (arr) {
   for (let i = 0; i<arr.length; i++) {
    var j = Math.floor(Math.random() * arr.length)
    [arr[i], arr[j]] = [arr[j], arr[i]]
  }
}

# 插入排序

将第一个元素作为有序序列,遍历后面未排序的元素,一个个与已经排序的元素做比较,并且排到前面

插入排序

function insertionSort(arr) {
    for (var i = 1; i < arr.length; i++) {
        var element = arr[i];
        for (var j = i - 1; j >= 0; j--) {
            var tmp = arr[j];
            var order = tmp - element;
            if (order > 0) {
                arr[j + 1] = tmp;
            } else {
                break;
            }
        }
        arr[j + 1] = element;
    }
    return arr;
}

var arr = [6, 5, 4, 3, 2, 1];
console.log(insertionSort(arr));

插入排序 (opens new window)

# 快速排序

  1. 选择一个元素作为"基准"
  2. 小于"基准"的元素,都移到"基准"的左边;大于"基准"的元素,都移到"基准"的右边。
  3. 对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

快速排序

var quickSort = function(arr) {
  if (arr.length <= 1) { return arr; }
    // 取数组的中间元素作为基准
  var pivotIndex = Math.floor(arr.length / 2);
  var pivot = arr.splice(pivotIndex, 1)[0];

  var left = [];
  var right = [];

  for (var i = 0; i < arr.length; i++){
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return quickSort(left).concat([pivot], quickSort(right));
};

《快速排序(Quicksort)的Javascript实现》 (opens new window)

# 模块

# AMD 与 CMD 区别

  1. 写法上的区别,AMD推崇依赖前置, CMD 推崇依赖就近

  2. 加载时机: AMD 是提前执行,CMD 是延迟执行

# CommonJS 与 AMD区别

CommonJS 规范加载模块是同步的, Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式

如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范

# ES6 与 CommonJS

  1. CommonJS是值的拷贝, es6是值的动态引用,不会缓存值

  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成