JavaScript-吃透函数,原型对象,原型链,继承

241 阅读8分钟

对象

对象创建的方式

//new
var person = new Object()
//对象字面量
var person ={
    name:"a",
    age:29,
}

属性

数据属性

数据属性包含一个数据值的位置,可以读取和写入

  • [[Configurable]]: (1)能否通过delete删除属性,从而重新定义属性 (2)能否修改属性的特性,修改为访问器属性。(new, 对象字面量创建对象时,这个属性默认的true)
  • [[Enumerable]]: 能否通过for-in循环返回属性(new, 对象字面量创建对象时,这个属性默认的true)
  • [[Writable]] :能否修改属性的值 (new, 对象字面量创建对象时,这个属性默认的true)
  • [[value]]: 包含这个属性的数据值,读取属性值的时候在这个位置读,写入属性值的时候,把新值保存在这个位置,默认undefined.

修改这个几个属性,可以使用ECMAScript5的Object.defineProperty(属性所在对象,属性的名字,描述符对象{configurable,enumerable,writable,value})方法

//不可以写入
var person ={}
Object.defineProperty(person,"name",{
    writable:false,
    value: "Nicholas"
})
console.log(person.name);//'Nicholas'
person.name='Greg';
console.log(person.name);//'Nicholas'
//不可以配置, 一旦定义为不可配置,就无法再重新配回可配置了,会报错
Object.defineProperty(person,"name",{
    configurable:false
})
delete person.name;
console.log(person.name);//Nicholas

访问器属性

不包含数据值,不是必需,包含一对函数

  • getter, 读取访问属性时,会调用getter函数
  • setter,写入访问器属性时,会调用setter函数并传入新值

4个特性

  • [[Configurable]]: (1)能否通过delete删除属性,从而重新定义属性 (2)能否修改属性的特性,修改为数据属性。(直接在对象里定义的属性时,这个属性默认的true)

  • [[Enumerable]]: 能否通过for-in循环返回属性(直接在对象里定义的属性时,这个属性默认的true)

  • [[Get]]:在读取属性时,调用的函数,默认是undefined

  • [[Set]] 在写入属性时,调用的函数,默认是undefined

    访问器属性不可以直接定义,必须使用Object.defineProperty()

    如果只指定了getter,就不能写,只指定setter,就不能读

    var book = {
        _year:2004,
        edition:1
    }
    Object.defineProperty(book,'year',{
        get:function(){
            return this._year;
        },
        set:function(newValue){
            if(newValue >2004){
                this._year = newValue;
                this.edition +=newValue - 2004
            }
        }
    })
    book.year = 2005;
    alert(book.edition);//2

    定义多个属性

    ECMAScript 5定义了Object.defineProperties()方法

    var book ={}
    Object.defineProperties(book,{
        year:{
            writable:true,
            value:2004
        },
        edition:{
            writable:true,
            value:1
        }
    })
    

    读取属性的特性

    Object.getOwnPropertyDescriptor()

创建对象

方式

工厂模式

function createPerson(name,age,job){
    var o =new Object();
    o.name = name;
    o.sayName = function(){
        console.log(this.name)
    }
    return o;
}

构造函数模式

  • 必须使用new操作符

    • 过程

      • 创建一个新对象
      • 将构造函数的作用域赋给对象(因此this就指向了这个新对象)
      • 执行构造函数中的代码(为这个新对象添加属性)
      • 返回新对象
function Person(name,age,job){
    this.name = name;
    this.sayName = function(){
        console.log(this.name)
    }
}
  • 将构造函数当作函数

    • 任何函数,只要通过new 操作符来调用,它就可作为构造函数

    • 如果不通过new, 和普通函数没有区别

      //当作构造函数使用
      var person = new Person('a',29,'s')
      person.sayName();//'a'
      //普通函数
      Person('a',27,'s') //添加的window
      window.sayName();//'Greg'
  • 构造函数的缺点(使用原型模式解决)

    • 每个方法,都要在每个实例上重新创建一遍

    • 有this对象在,不需要在执行代码,就把函数绑定到对象上面

      function Person(name,age,job){
          this.name = name;
          this.sayName =sayName;//把sayName定义出去,使用this 来判定其作用域环境
      }
      function sayName(){
              console.log(this.name)
          }
      

原型模式

每个函数都有一个prototype(原型)属性, 是一个指针,指向一个对象(原型对象)

原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法

  • 使用原型对象的好处是可以让所有的对象实例共享它所包含的属性和方法

    function Person(){
        
    }
    Person.prototype.name = 'a';
    Person.prototype.sayName = function(){
        console.log(this.name)
    };
    

理解原型对象

  1. 创建一个新函数,如function Person(){} , 通过一定的规则创建一个prototype属性,指向原型对象
  2. 原型对象,自动获得一个constructor构造函数属性,是一个指针,指向prototype属性所在的函数,则Person(){}
  3. 原型对象会从Object基础其它方法
  4. 当调用构造函数创建一个新实例person1后,该实例会有一个指针[[Prototype]] ,指向构造函数的原型对象,则Person Prototype
  5. 新的实例,可以包含自己的属性和方法,也可以调用原型对象上的方法,属性
  6. 通过Person.prototype.isPrototypeOf(person1)判断实例与原型对象的关系, Object.getPrototypeOf(实例),返回实例的原型
  7. 读取对象某个属性,分两步,一对象实例本身开始,找到返回,如果实例没有找到,二从原型对象中搜索
  8. 对象实例拥有的与原型对象中相同属性名的属性,实例上的属性,会屏蔽原型中的那个属性,访问的是实例上的属性,防止访问原型对象的同名属性, 实例.hasOwnProperty('fieldName')判断是否有实例上的属性,

函数与原型对象,实例关系.png

  1. 判断属性是否在原型上

    • 单独使用,in 'name' in 实例,可以判断属性在实例,或者原型上

    • hasPrototypeProperty可以判断,原型上是否有该属性

    1. 获取属性

      • Object.keys 获取可枚举的属性
      • Object.getOwnPrototypeNames()返回所有实例属性,无论是否可枚举
      function Person(){
      ​
      }
      ​
      Person.prototype.name='a'Person.prototype.age = 1const keys = Object.keys(Person.prototype)//['name','age']
      const keys1 = Object.getOwnPropertyNames(Person.prototype);//['constructor','name','age']
      

    更简单的原型语法

    使用对象字面量来写,相当于重写了原型对象,原来实例的[[Proptotype]]指针切断了构造函数与最初原型的联系

    优点:简写

    缺点:constructor属性不再指向Person,但是可以显示指定,但是会导致constructor可枚举,原生的不可枚举

    function Person(){
        
    }
    Person.prototype = {
        constructor:Person//显示指定
        name:'N',
        age:29
        sayName:function(){
            console.log(this.name)
        }
    }
    ​
    

    原型的动态性

    对象字面量重写原型对象,会切断现有原型与任何之前已经存在的对象实例之间的联系

    function Person(){};
    var friend = new Person();
    Person.prototype.sayHi = function(){console.log('hi')}
    friend.sayHi();//'hi'Person.prototype = {
        constructor:Person,
        name:'N',
        sayName:function(){
            console.log('a')
        }
    }
     //friend.sayName()//error
     var friend1 = new Person()
    friend1.sayName()//'a'
    

原型对象与对象字面量.png

原生对象的原型

所有的原生的引用类型,如Object, Array, String都是采用了 原型模式来创建

所以可以修改,添加新的方法到原生的原型对象上

原型对象的问题

包含引用类型的值的属性来说,例如数组,会导致所有实例都会有相同的值

function Person(){}
Person.prototype = {
    constructor:Person,
    name:'a'
    friends:['a','b']
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('van')
consolo.log(person1.friends===person2.friends)//true

组合使用构造函数模式和原型模型模式(节省内存)

  • 构造函数模式定义实例属性
  • 原型模式用于定义方法和共享属性
function Person(name,age,job){
    this.name=name;
}
Person.prototype={
    constructor:Person,
    sayName;function(){
        console.log(this.name);
    }
}

动态原型模型模式

function Person(name,age,job){
    this.name = name;
    this.age = age;
    if(typeof this.sayName !='function'){
        Person.prototype.sayName = function(){
            console.log(this.name);
        }
    }
}
​

寄生构造函数模式

可以使用new, 又类似工厂模式

function Person(name){
    var o = new Object();
    o.name=name;
    o.sayName = function(){
        console.log(this.name);
    }
    return o;
}
var f = new Person('a')

稳妥构造函数模式

稳妥在于

  • 没有公共属性
  • 新创建对象的实例方法不引用this
  • 不使用new操作符调用构造函数
function Person(name){
    var o = new Object();
    //可以在这里定义私有变量和函数
    //添加方法
    o.sayName= function(){
        console.log(name);
    };
    return o;
}
var f = Person('n');
f.sayName();

继承

ECMAScript只支持实现继承,依靠原型链来实现

将原型对象等于另一个类型的实例,实例拥有[[prototype]]指针指向了另一个实例的原型对象, 如果另一一个原型对象又是别的实例,就形成了一条原型的链条

原型链.png

实例可以使用原型链上的所有方法

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subproperty = false;
}
//继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
    return this.subproperty
}
var instance = new SubType();
console.log(instance.getSuperValue())//true
  • instance.constructor 指向SuperType, 因为原来的SubType.prototype中的constructor 被重写了缘故
  • 调用实例属性,会先从对象实例上找,找不到,就一直向上搜索原型链的原型对象
  • 所有的函数默认的原型是Object的实例,所以SuperType Prototype有一个[[prototype]]指针指向Object Prototype
  • instanceof 判断出现过的构造函数
  • 通过原型链实现继承时,不能使用对象字面量创建原型方法,这样做会重写原型链

原型链的问题

  • 属性为引用类型会被所有的实例共享

    function SuperType(){
        this.colors=['red','blue']
    }
    function SubType(){
    ​
    }
    SubType.prototype = new SuperType();
    const instance1 = new SubType()
    instance1.colors.push('black')
    var instance2 = new SubType();
    console.log(instance1.colors == instance2.colors)//true
    
  • 没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数

原型链的问题解决- 借用构造函数

function SuperType(name){
    this.name =name
    this.colors=['red','blue']
}
function SubType(a){
 SuperType.call(this,a)
}
SubType.prototype = new SuperType();
const instance1 = new SubType('a')
instance1.colors.push('black')
var instance2 = new SubType('b');
console.log(instance1.colors == instance2.colors)//false
console.log(instance1.name === instance2.name)//false
  • 问题: 函数无法复用

解决-组合继承

原型链和借用构造函数的组合

function SuperType(name){
    this.name =name
    this.colors=['red','blue']
}
function SubType(a){
 SuperType.call(this,a)
 this.age = 1
}
SubType.prototype = new SuperType();
SuType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log('a')
}
const instance1 = new SubType('a')
instance1.colors.push('black')
var instance2 = new SubType('b');
console.log(instance1.colors == instance2.colors)//false
console.log(instance1.name === instance2.name)//false

原型式继承

function object(o){
    function F(){}
    F.prototype=o;//o为基础,实例作为新的对象的原型
    return new F();
}
  • ECMAScript5 通过Object.create()方法,规范了原型式继承

    var person = {
        name:'a',
        friends:['a','b']
    }
    var person1 = Object.create(person)
    person1.name='g'//相当于在实例person1,添加了name属性,屏蔽了person上的name
    person1.friends.push('c')
    var person2 = Object.create(person)
    person2.name='b'
    person2.friends.push('d')
    console.log(person2.name===person1.name)//false
    console.log(person1.friends===person2.friends)//true

寄生式继承

不仅拥有了person的方法,属性,还可以自己添加新的方法,属性

var person = {
    name:'a',
    friends:['a','b']
}
​
​
function createAnother(original){
    var clone = object(original);
    clone.sayHi= function () {
        console.log('a')
    }
}
​
var p1 = createAnother(person);
console.log(p1.friends === person.friends)

寄生组合式继承

function SuperType(name){
    this.name =name
    this.colors=['red','blue']
}
function SubType(a){
 SuperType.call(this,a)
 this.age = 1
}
SubType.prototype = new SuperType();
SuType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log('a')
}
function createAnother(subType,superType){
    var clone = object(superType.prototype);//创建对象
    clone.constructor= subType;//增强对象
    subType.prototype = clone;//指定对象
}
​