JavaScript 学习笔记(一)

学习 JavaScript深入系列 的笔记

原型与原型链

无论再任何情况,只要创建了一个新函数,就会根据一组特定规则为该函数创建一个prototype属性,每一个对象都会从原型"继承"属性。这个属性指向函数的原型对象。在默认情况下,所有的原型对象会自动获得一个constructor属性,这个属性是指向prototype属性所在的函数的指针。

__proto__

这是每一个JavaScript对象(除了 null )都具有的一个属性,这个属性会指向该对象的原型。 Player.prototype === Curry.__proto__ // Curry 是 Player 的实例

constructor

constructor 属性是专门为 function 而设计的,它存在于每一个 function 的 prototype 属性中。这个 constructor 保存了指向 function 的一个引用。 例如我们在定义 Player JavaScript 内部会为该函数添加prototype属性,然后为 prototype 添加一个 constructor 属性,并且该属性保存指向函数 Player 的一个引用,写成代码就是 Player.prototype.constructor === Player

// 构造函数
function Player(){
}
// 通过 new 操作符 创建一个Curry实例对象 
var Curry = new Player();
// 实例对象的  __proto__ 属性指向 构造函数 Player 的 prototype
console.log(Player.prototype === Curry.__proto__);               //true
// 构造函数 Player 的 prototype.constructor 指向 构造函数 Player
console.log(Player.prototype.constructor === Player);            //true
// 构造函数的 __proto__ 指向 对象的 prototype
console.log(Player.prototype.__proto__ === Object.prototype);    //true
// 对象的 __proto__ 指向 null
console.log(Object.prototype.__proto__ === null);                //true
复制代码

图中蓝色的就是原型链,它是一种相互关联的原型组成的链状结构。

当我们在实例对象访问其属性的时候,例如Curry.name,它会先从对象实例本身寻找,如果找到就返回,如果没找到就会顺着原型链往上查找,找到就返回,不找到就一直往上查找。

function Player(name){
    if(name)this.name = name;
}
// 通过 new 操作符 创建一个Curry实例对象 
var Curry = new Player('Curry');
var James = new Player();

console.log(Curry);
//Curry.__proto__.name = 'NBA player';
Player.prototype.name = 'NBA player';
console.log(Curry.name); // Curry
console.log(James.name); // NBA player
复制代码

执行上下文栈

静态作用域

JavaScript 采用的 作用域是静态的作用域,函数的作用域在函数定义的时候就决定了。

可执行代码

JavaScript 执行代码的时候并非一行一行的分析代码,而是一段一段的执行,当执行一段代码的时候,会进行一个准备工作,比如变量提升跟函数提升。

  • a 由于是用 var 定义,在JavaScript引擎中,会先执行 var a; 然后执行console.log(a),再执行 a = 1,所以打印出来是 undefined
  • 由于 JavaScript 存在函数提升,会先执行 function b(){console.log('function-b')},再往下执行。
// 变量提升
console.log(a);
var a = 1;

var b = 1;
// 函数提升
function b(){
    console.log('function-b');
}
console.log(b);
复制代码

可执行代码分为三种,全局代码,函数代码,eval代码。当执行到一个函数的时候,会进行准备工作,称为执行上下文。

JavaScript 引擎创建了上下文栈来管理上下文。

试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
ECStack = [
    'globalContext',
    'checkscopeContext',
    'fContext',
    'pop',
    'pop'
]
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
ECStack = [
    'globalContext',
    'checkscopeContext',
    'pop',
    'fContext',
    'pop'
]
复制代码

每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

变量对象

变量对象是执行上下文相关的数据作用域,存储了上下文中定义的变量跟函数声明。

不同的执行上下文的变量对象是不同的。

全局上下文

全局对象只是一个对象,而不是类。既没有构造函数,也无法实例化一个新的全局对象。

  • 可以通过 this 引用,在客户端JavaScript中,全局对象是 Window,在node环境中是 Global
  • 全局对象其实是由 Object 实例化的一个对象
  • 预定义了一些函数属性
  • 作为全局变量的宿主 a === this.a
  • 客户端 JavaScript 中,全局对象有 window 属性指向自身。
  • 全局上下文的变量对象就是全局对象
函数上下文

在函数上下文中,用活动对象 ( Activation Object, AO ) 来表示变量对象。一般在函数上下文创建的时候创建, 通过函数的 arguments 属性初始化,arguments 属性值是 Arguments 对象。

执行过程
  • 进入执行上下文

    当进入执行上下文时,此时还未执行代码。

    变量对象会包含

    • 函数的所有形参
      • 由名称和对应值组成的一个变量对象的属性被创建,没有实参,属性值设为 undefined
    • 函数声明
      • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
      • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
    • 变量声明
      • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
      • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
function foo(a){
    var b = 2;
    function c(){}
    var d = function(){}
}
foo(1);
AO = {
    arguments:{
        0:1,
        length:1
    },
    a:1,
    b:undefined,
    c:reference to function c(){},
    d:undefined
}
复制代码
  • 代码执行
    • 代码执行的时候,会顺序执行代码,根据代码改变变量的值
    AO = {
        arguments:{
            0:1,
            length:1
        },
        a:1,
        b:3,
        c:reference to function c(){},
        d:reference to FunctionExpression "d"
    }
    复制代码
变量对象的创建过程
  • 全局上下文的变量对象初始化是全局对象
  • 函数上下文的变量对象初始化只包括 Arguments 对象
  • 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  • 在代码执行阶段,会再次修改变量对象的属性值

作用域链

查找变量的时候,会在当前上下文的变量对象中查找,如果没有找到,就会从父级的执行上下文中查找,一直查找到全局上下文变量对象。这个由多个执行上下文构成的链表就叫作用域链。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
复制代码

1.首先 checkscope 函数被创建,保存作用域链到内部属性 [[scope]]

checkscope.[[scope]] = [
    globalContext.VO
]
复制代码

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈。

ECStack = [
    checkscopeContext,
    globalContext
]
复制代码

3.checkscope 函数并不立即执行,开始准备工作,第一步复制函数 [[scope]] 属性创建作用域链

checkscopeContext = {
    Scope:checkscope.[[Scope]]
}
复制代码

4.创建活动对象,初始化活动对象,加入形参,函数声明,变量声明

checkscopeContext = {
        AO: {
        arguments: {
            length: 0
        },
        scope: undefined
    },
    Scope:checkscope.[[Scope]]
}
复制代码

5.将活动对象压入 checkscope 作用域链顶端

checkscopeContext = {
        AO: {
        arguments: {
            length: 0
        },
        scope: undefined
    },
    Scope:[AO,Scope]
}
复制代码
  1. 准备工作做完,开始执行函数,随着函数的执行,改变 AO 的属性值
checkscopeContext = {
       AO: {
       arguments: {
           length: 0
       },
       scope: 'local scope'
   },
   Scope:[AO,Scope]
}
复制代码

7.查找到 scope ,返回函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
]
复制代码

this

函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。

在绝大多数情况下,函数的调用方式决定了this的值。this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。ES5引入了bind方法来设置函数的this值,而不用考虑函数如何被调用的,ES2015 引入了支持this词法解析的箭头函数(它在闭合的执行上下文内设置this的值)。

Reference

Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。

尤雨溪大大:Reference 是一个 Specification Type,也就是“只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。 Reference 由三个部分构成

  1. base value
  2. reference name
  3. strict reference

base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。

referenced name 就是属性的名称。

var foo = 1;
// 对应的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

var foo = {
    bar: function () {
        return this;
    }
};
 
foo.bar(); // foo
var bar = foo.bar;
bar();

// foo.bar对应的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

// bar对应的Reference是:
var BarReference = {
    base: EnvironmentRecord,
    name: 'bar',
    strict: false
};
复制代码

1.GetBase

GetBase(V). Returns the base value component of the reference V. 返回 reference 的 base value。

2.IsPropertyReference

IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false. 如果 base value 是一个对象,就返回true。

3.GetValue 除此之外,紧接着在 8.7.1 章规范中就讲了一个用于从 Reference 类型获取对应值的方法: GetValue。 简单模拟 GetValue 的使用:

var foo = 1;
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

GetValue(fooReference) // 1;
复制代码

GetValue 返回对象属性真正的值,但是要注意:

调用 GetValue,返回的将是具体的值,而不再是一个 Reference

如何确定this的值

1.计算 MemberExpression 的结果赋值给 ref

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
复制代码

什么是 MemberExpression?

MemberExpression :

  • PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
  • FunctionExpression // 函数定义表达式
  • MemberExpression [ Expression ] // 属性访问表达式
  • MemberExpression . IdentifierName // 属性访问表达式
  • new MemberExpression Arguments // 对象创建表达式
function foo() {
    console.log(this)
}

foo(); // MemberExpression 是 foo

function foo() {
    return function() {
        console.log(this)
    }
}

foo()(); // MemberExpression 是 foo()

var foo = {
    bar: function () {
        return this;
    }
}

foo.bar(); // MemberExpression 是 foo.bar
复制代码

简单理解 MemberExpression 其实就是()左边的部分。 2.判断 ref 是不是一个 Reference 类型。 关键在于看规范如何处理各种 MemberExpression 返回的结果是否是一个 Reference 类型

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar());
/*
    foo.bar 是 MemberExpression 的计算结果
    该表达式返回了 一个 Reference 类型
    Reference = {
        base:foo,
        name:'bar',
        strict:false
    }
    如果 ref 是 Reference,并且isPropertyReference(ref) == true 那this的值为 GetBase(ref)
    foo是一个对象,所以 isPropertyReference(ref) = true
    GetBase(ref) 返回 base 的值,所以GetBase(ref) = foo;
    this = foo
*/
//示例2
console.log((foo.bar)());
/*
    (foo.bar)()
    (foo.bar)是 MemberExpression 的计算结果
    但是()并不会对 MemberExpression 进行计算
    所以跟示例1 的结果一致
*/
//示例3
console.log((foo.bar = foo.bar)());
/*
    (foo.bar = foo.bar) 是 MemberExpression 的计算结果
    规范 11.13.1 根据规范中的计算第三步
    令 rval 为 GetValue(rref).
    所以 (foo.bar = foo.bar) 并不是一个 Reference类型
    2.3 如果 ref 不是Reference,那么 this 的值为 undefined
    在非严格模式下,this值为 undefined 会隐式的被转换成全局对象
*/
//示例4
console.log((false || foo.bar)());
/*
    (false || foo.bar) 是 MemberExpression 的计算结果
    根据规范 11.11 二元逻辑运算符
    计算的第二步
        令 lval 为 GetValue(lref).
    所以 (false || foo.bar) 并不是一个 Reference类型
*/
//示例5
console.log((foo.bar, foo.bar)());
/*
    (foo.bar, foo.bar) 是 MemberExpression 的计算结果
    根据规范 11.14 逗号运算符,
    第二步的计算结果
        Call GetValue(lref).
    所以 (false || foo.bar) 并不是一个 Reference类型
*/
//示例6
function foo() {
    console.log(this)
}

foo(); 
/*
    foo 是 MemberExpression ;
    规范 10.3.1 Identifier Resolution,会返回一个 Reference 类型的值:
    fooReference = {
        base:EnvironmentRecord,
        name:'foo',
        strict:false
    }
    因为 base value 是 EnvironmentRecord 并不是一个Object类型,所以 isPropertyReference(ref) 为 false 进入下一个判断。
    
    如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
    
    ImplicitThisValue()
     声明式环境记录项永远将 undefined 作为其 ImplicitThisValue 返回。
    
    所以最后 this 值为 undefined
*/

复制代码

闭包

从理论角度:闭包是指那些能够访问自由变量的函数。

自由变量:是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

从实践角度:

  • 即使创建它的上下文已经销毁,它依然存在(内部函数从父函数中返回)
  • 代码中引用了自由变量

分析此段代码的执行上下文

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
复制代码
  1. 执行全局的代码,创建全局执行上下文,并压入执行上下文栈
    ECStact = [
        globalContext
    ]
    复制代码
  2. 全局上下文初始化
    globalContext = {
        VO:[global],
        Scope:[globalContext.VO],
        this.globalContext.VO
    }
    复制代码
  3. 初始化的同时,checkscope 函数被创建,保存作用域链到函数内部属性 [[scope]]
    checkscope.[[scope]] = [
        globalContext.VO
    ]
    复制代码
  4. 执行 checkscope 函数,创建 checkscope 执行上下文,函数执行上下文压入执行上下文栈
    ECStack = [
        checkscopeContext,
        globalContext
    ];
    复制代码
  5. checkscope 初始化,复制函数 [[scope]] 属性到作用域链,用 arguments 创建活动对象,活动对象 加入形参,函数声明,变量声明,将活动对象压入 checkscope 作用域顶端
     checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
    复制代码
  6. f 函数被创建,保存作用域链到函数内部属性 [[scope]]
    f.[[scope]] = [
       checkscopeContext.AO,
       globalContext.VO
    ]
    复制代码
  7. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
    ECStack = [
        globalContext
    ];
    复制代码
  8. 执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
    ECStack = [
       fContext,
       globalContext
    ];
    复制代码
  9. f 初始化,复制函数 [[scope]] 属性到作用域链,用 arguments 创建活动对象,活动对象 加入形参,函数声明,变量声明,将活动对象压入 f 作用域顶端
     fContext = {
        AO: {
            arguments: {
                length: 0
            },
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
    复制代码
  10. 闭包的源头就来自于作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明了 f 函数应用了 checkscopeContext.AO 的值的时候,即便 checkscopeContext 被销毁了,JavaScript 依然会让 checkscopeContext.AO 活在内存中,所以可以通过作用域链访问到它。正因为这一点,从而实现了闭包的概念。
    fContext = {
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
    }
    复制代码
  11. f 函数执行,沿着作用域链查找到 scope 返回,函数执行结束,从执行上下文栈中,弹出 f 函数执行上下文。
    ECStack = [
        globalContext
    ];
    复制代码

按值传递

所有函数的参数都是按值传递的。

按值传递:把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

call 和 apply

call() 跟 apply() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。他们两个的区别是 apply 需要传数组作为调用方法的参数

模拟实现 call 和 apply
  1. 将函数设为对象属性
  2. 执行函数
  3. 删除该函数
Function.prototype.newCall = function(context){
        //第一步,把函数挂载到context上
        // 获取 context
        context = context || window
        context.fn = this
        // 通过arguments获取参数
        var args = []
        var len = arguments.length
        var result
        if(len < 2){
            result = context.fn()
        }else{
            for(var i = 1; i < len; i++){
                args.push('arguments[' + i + ']')
            }
            //第二步,执行函数 args 会自己调用 Array.toString()
            result = eval('context.fn(' + args + ')')
        }
            
        //第三步,删除属性
        delete context.fn
        return result
    }
    Function.prototype.newApply = function(context,arr){
        //第一步,把函数挂载到context上
        // 获取 context
        context = context || window
        context.fn = this
        // 通过 arr 获取参数
        var result
        if(!arr){
            result = context.fn()
        }else{
            var args = []
            for(var i = 0, len = arr.length; i < len; i++){
                args.push('arr[' + i + ']')
            }
            //第二步,执行函数 args 会自己调用 Array.toString()
            result = eval('context.fn(' + args + ')')
        }
        //第三步,删除属性
        delete context.fn
        return result
    }
    var value = 'window'
    var foo = {
        value:'foo'
    }
    function bar(name,age){
        return {
            value: this.value,
            name: name,
            age: age
        }
    }
    function empty(){
        return {
            value: this.value
        }
    }
    console.log(bar.newCall(foo,'Curry',30))
    console.log(bar.newApply(foo,['Curry',30]))
    console.log(empty.newCall(foo))
    console.log(empty.newApply(foo))
    console.log(empty.newCall(null))
    console.log(empty.newApply(null))
复制代码

bind方法

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

 Function.prototype.newBind = function(context){
        if (typeof this !== "function") {
            throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
        }
        var self = this
        //取出先传入 参数
        var args = Array.prototype.slice.newCall(arguments, 1);
        console.log(args);
        var fNOP = function () {};
        //返回一个函数
        var fBound = function(){
            //取出调用的时候的参数
            var bindArgs = Array.prototype.slice.call(arguments);
            console.log(bindArgs);
            // 当作为构造函数时,this 指向实例,此时结果为 true,
            // 将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
            // 以上面的是 demo 为例,
            // 如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,
            // 将 null 改成 this ,实例会具有 name 属性
            // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
            // return self.newApply(context, args.concat(bindArgs))
            console.log(this);
            console.log(context);
            return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs))
        }
        // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound
    }
    var bindFoo = empty.newBind(foo,1,2,3,4,5,6) 
    var bindFoo2 = bar.newBind(foo,'Curry')
    console.log(bindFoo())
    console.log(bindFoo2(30))
    console.log(new bindFoo2(30))
复制代码

new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

function objectFactory() {
    var obj = new Object(),//从Object.prototype上克隆一个对象
    Constructor = [].shift.call(arguments);//取得外部传入的构造器
    obj.__proto__ = Constructor.prototype;//指向正确的原型
    var ret = Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性
    return typeof ret === 'object' ? ret : obj;//确保构造器总是返回一个对象
};
复制代码

类数组对象

类数组对象:拥有一个 length 属性和若干索引属性的对象

var arrayLike = {
    0:'name',
    1:'age',
    2:'sex',
    length:3
}
复制代码

类数组对象跟数组的读取跟长度是一样的,也可以通过for遍历,但是不能直接调用数组的方法

但是可以通过 Function.call 间接调用

Array.prototype.forEach.call(arrayLike,function(item){console.log(item)})
复制代码

类数组转换成 数组对象

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

复制代码

Arguments对象

Arguments对象只定义于函数体内,包括函数的参数和其他属性,在函数体内,arguments 指代该函数的 Arguments对象

  • length 属性
    • 表示实参的长度
  • callee 属性
    • 通过它可以调用函数本身
  • arguments 和对应参数的绑定
    • 传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享
  • 传递参数
    • 可以将参数传递给另一个函数

创建对象的各种方式

工厂模式
/**
 * 工厂模式
 * 函数 createPerson 能够接受参数来构建一个包含必要信息的对象,可以无数次的调用这个函数。而每次都返回一个对象
 * 优点 解决了创建多个相似对象的问题
 * 缺点 所有对象都指向一个原型,没有办法识别不同对象
 */ 
function createPerson(name,age,job) {
    var o = {};
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log('my name is ' + o.name)
    }
    return o;
}
var Curry = createPerson('Curry',30,'NBA Player')
Curry.sayName()
var James = createPerson('James',34,'NBA Player')
James.sayName()
复制代码

构造函数模式
/**
 * 构造函数模式
 * ECMAScript 可以通过构造函数来创建特定类型的对象 要通过new操作符
 * new 操作符调用函数会有四个步骤
 * 1. 创建一个新对象
 * 2. 将构造函数的作用域赋给新对象 改变this指向
 * 3. 执行构造函数中的代码 添加属性
 * 4. 返回新对象
 * 优点 实例可以识别为一个特定的类型
 * 缺点 每次创建实例的时,每个方法都需要创建一次
 */
function Person(name,age,job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log('my name is ' + this.name)
    }
}
var Curry = new Person('Curry',30,'NBA Player')
Curry.sayName()
var James = new Person('James',34,'NBA Player')
James.sayName()
复制代码

原型模式
/**
 * 原型模式
 * 由于创建的每个函数都有一个 prototype,可以使用原型对象,让所有的对象实例共享它所包含的属性方法
 * 优点,方法不会重新创建
 * 缺点,所有属性跟方法都共享,不能初始化参数
 */

function Person(name,age,job) {
    
}
Person.prototype.name = 'Curry';
Person.prototype.age = 30;
Person.prototype.job = 'NBA Player';
Person.prototype.sayName = function(){
    console.log('my name is ' + this.name)
}
var Curry = new Person()
Curry.sayName() // my name is Curry
var James = new Person()
James.sayName() // my name is Curry
复制代码

原型模式优化
/**
 * 原型模式优化
 * 由于创建的每个函数都有一个 prototype,可以使用原型对象,让所有的对象实例共享它所包含的属性方法
 * 优点,封装性好了一点
 * 缺点,重写了原型,丢失了constructor属性,所有属性跟方法都共享,不能初始化参数
 */

function Person(name,age,job) {
    
}
Person.prototype = {
    name: 'Curry',
    age: 30,
    job: 'NBA Player',
    sayName: function(){
        console.log('my name is ' + this.name)
    }
}
var Curry = new Person()
Curry.sayName() // my name is Curry
var James = new Person()
James.sayName() // my name is Curry
复制代码
原型模式优化
/**
 * 原型模式优化
 * 由于创建的每个函数都有一个 prototype,可以使用原型对象,让所有的对象实例共享它所包含的属性方法
 * 优点,实例可以通过constructor属性找到所属构造函数
 * 缺点,重写了原型,所有属性跟方法都共享,不能初始化参数
 */

function Person(name,age,job) {
    
}
Person.prototype = {
    name: 'Curry',
    age: 30,
    job: 'NBA Player',
    sayName: function(){
        console.log('my name is ' + this.name)
    }
}
Object.defineProperty(Person.prototype, "constructor",{
    enumerable:false,
    value:Person
})
var Curry = new Person()
Curry.sayName() // my name is Curry
var James = new Person()
James.sayName() // my name is Curry
复制代码
组合模式
/**
 * 组合模式
 * 使用构造函数跟原型模式双剑合璧。构造函数模式用于定义实例属性,原型属性定义方法和共享属性。
 * 优点,该共享的共享,该私有的私有,使用最广泛的方式
 * 缺点,有的人就是希望全部都写在一起,即更好的封装性
 */

function Person(name,age) {
    this.name = name;
    this.age = age;
}
Person.prototype = {
    job: 'NBA Player',
    sayName: function(){
        console.log('my name is ' + this.name)
        console.log('my age is ' + this.age)
        console.log('my job is ' + this.job)
    }
}
Object.defineProperty(Person.prototype, "constructor",{
    enumerable:false,
    value:Person
})
var Curry = new Person('Curry',30)
Curry.sayName() // my name is Curry
var James = new Person('James',34)
James.sayName() // my name is James
复制代码

动态原型模式
/**
 * 动态原型模式
 * 为了解决独立的构造函数和原型,动态原型模式,把信息封装到构造函数中,而且通过在构造函数初始化原型,
 * 同时使用构造函数跟原型的优点
 */

function Person(name,age) {
    this.name = name;
    this.age = age;
    this.job = 'NBA Player';
    if(typeof this.sayName != 'function'){
        Person.prototype.sayName = function(){
            console.log('my name is ' + this.name)
            console.log('my age is ' + this.age)
            console.log('my job is ' + this.job)
        }
    }
}

var Curry = new Person('Curry',30)
Curry.sayName() // my name is Curry
var James = new Person('James',34)
James.sayName() // my name is James
复制代码

寄生-构造函数-模式
/**
 * 寄生-构造函数-模式
 * 创建一个函数,该函数的作用仅仅在封装创建对象的代码,然后返回新创建的对象
 * 其实就是工厂模式 + new
 * 平时没事不要用
 */

function Person(name,age) {
    var o = {};
    o.name = name;
    o.age = age;
    o.job = 'NBA Player';
    o.sayName = function(){
        console.log('my name is ' + this.name)
        console.log('my age is ' + this.age)
        console.log('my job is ' + this.job)
    }
    return o;
}

var Curry = new Person('Curry',30)
Curry.sayName() // my name is Curry
var James = new Person('James',34)
James.sayName() // my name is James
复制代码

稳妥函数模式
/**
 * 稳妥-构造函数-模式
 * 稳妥对象 指那些没有公共属性,而且其方法不引用this的对象
 * 它与寄生构造有两点不同,
 * 1.新创建的实例方法不引用 this 
 * 2.不使用 new 操作符调用构造函数
 * 稳妥模式一般用于一些安全的环境中
 * 稳妥构造函数模式也跟工厂模式一样,无法识别对象所属类型。
 */

function Person(name,age) {
    var o = {};
    o.sayName = function(){
        console.log('my name is ' + name)
        console.log('my age is ' + age)
    }
    return o;
}

var Curry = Person('Curry',30)
Curry.sayName() // my name is Curry
var James = Person('James',34)
James.sayName() // my name is James
复制代码

继承

ECMAScript 支持实现继承,继承的主要方式是依靠原型链实现

原型链基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法

原型链继承
/**
 * 原型链继承
 * 它存在两个缺点
 * 1.引用类型的属性被所有实例共享
 * 2.在创建 Child 的实例时,不能向 Parent 传参
 */
function Parent(){
    this.name = 'parent'
    this.names = ['parent']
}
Parent.prototype.getName = function(){
    console.log(this.name)
}
function Child(){

}
// 继承了 Parent
Child.prototype = new Parent();
var child = new Child();
child.names.push('child');
var parent = new Child();
console.log(parent.names); //['parent','child']
child.getName();
复制代码

借用构造函数
/**
 * 借用构造函数继承
 * 优点:
 * 1.避免了引用类型的属性被所有实例共享
 * 2.可以在 Child 中向 Parent 传参
 * 缺点
 * 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
 */
function Parent(name){
    this.name = name
    this.names = ['parent']
}
function Child(name){
    Parent.call(this,name)
}
// 继承了 Parent
var child = new Child('Curry');
child.names.push('child');
console.log(child.name); //Curry
console.log(child.names); //['parent','child']
var parent = new Child('James');
console.log(parent.name); //James
console.log(parent.names); //['parent']
复制代码

组合继承
/**
 * 组合继承
 * 原型链继承 + 经典继承 组合
 * 使用原型链实现对原型属性和方法的继承,借用构造函数来实现对实例属性的继承
 * 优点避免了原型跟借用构造函数的缺点
 */
function Parent(name){
    this.name = name
    this.names = ['parent']
}
Parent.prototype.getName = function(){
    console.log(this.name)
}
function Child(name,age){
    Parent.call(this,name)
    this.age = age
}
// 继承了 Parent
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.getAge = function(){
    console.log('age = '+this.age)
}
var child = new Child('Curry',30);
child.names.push('child');
child.getName(); // Curry
child.getAge();// 30
console.log(child.name);
console.log(child.names);// ['parent','child']
var parent = new Child('James',35);
child.getName();// James
child.getAge();// 35
console.log(parent.name);
console.log(parent.names);// ['parent']
复制代码

原型式继承
/**
 * 原型式继承
 * 就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
 * 缺点 引用类型的属性值始终都会共享相应的值
 */
function createObj(o) {
  function F(){}
  F.prototype = o;
  return new F();
}
var Parent = {
  name:'name',
  names:['parent'],
  sayName:function() {
    console.log(this.name)
  }
}
var child1 = createObj(Parent);
child1.name = 'Curry';
child1.names.push('child1');
child1.sayName(); //Curry
console.log(child1.names); // ['parent','child1']
var child2 = createObj(Parent);
child2.name = 'James';
child2.sayName();// James
console.log(child2.names);// ['parent','child1']
复制代码

寄生式继承
/**
 * 寄生式继承
 * 创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来增强对象,最好返回对象
 * 缺点 每次创建对象都会创建一遍方法。
 */
function createObj(o) {
  function F(){}
  F.prototype = o;
  return new F();
}
var Parent = {
  name:'name',
  names:['parent'],
  sayName:function() {
    console.log(this.name)
  }
}
function createChild(o,name){
  var clone = createObj(o);
  clone.name = name
  clone.sayName = function () {
    console.log(this.name);
  }
  return clone;
}
var child1 = createChild(Parent,'Curry');
child1.names.push('child1');
child1.sayName(); //Curry
console.log(child1.names); // ['parent','child1']
var child2 = createChild(Parent,'James');
child2.sayName();// James
console.log(child2.names);// ['parent','child1']
复制代码

寄生组合式继承
/**
 * 寄生式组合式继承
 * 通过构造函数来继承属性,通过原型链的混成方式来继承方法。
 * 组合继承是JavaScript最常用的继承模式,不过它的不足就是要调用两次父构造函数,
 * 一次是在创建子类型原型的时候,一次是在子类型构造函数内部
 * 牛逼之处
 * 这种方式的高效率体现它只调用了一次 Parent 构造函数,
 * 并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。
 * 与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。
 * 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
 */
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
function prototype(child, parent) {
    var prototype = object(parent.prototype); // 创建对象
    prototype.constructor = child; // 增强对象
    child.prototype = prototype; //指定对象
}

function Parent(name){
    this.name = name
    this.names = ['parent']
}
Parent.prototype.getName = function(){
    console.log(this.name)
}
function Child(name,age){
    Parent.call(this,name)
    this.age = age
}
// 关键三步 实现继承
// var F = function(){};
// F.prototype = Parent.prototype;
// Child.prototype = new F();

prototype(Child,Parent)

Child.prototype.getAge = function(){
    console.log('age = '+this.age)
}
var child = new Child('Curry',30);
child.names.push('child');
child.getName();//Curry
child.getAge();//age = 30
console.log(child.name); 
console.log(child.names); //[ 'parent', 'child' ]
var parent = new Child('James',35);
child.getName(); // James
child.getAge(); //age = 34
console.log(parent.name);
console.log(parent.names); //[ 'parent' ]
复制代码

结束

最后感谢 冴羽大神的 JavaScript深入系列 的系列,让我对JavaScript又有了更深的认识。实在推荐,结合红宝书跟深入系列,来回多读几次。

转载于:https://juejin.im/post/5bc96fcae51d450e6548dae3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值