JS--面向对象的程序设计

readme:笔记内容来自《JavaScript高级程序设计》(第3版)第6章的内容;大致内容可直接看小结部分;

文章目录

  • 理解对象
    • 属性类型
      • 数据属性
        • 4个特性
        • 修改属性
      • 访问器属性
        • 4个特性
      • 定义多个属性
      • 读取属性的特性
  • 创建对象
    • 工厂模式
    • 构造函数模式
    • 原型模式
      • 原型对象
        • 原型与 in 操作符
        • 更简单的原型语法
        • 原型的动态性
        • 原生对象的原型
        • 原型对象的问题
      • 构造函数模式 + 原型模式
      • 动态原型模式
      • 寄生构造函数模式
      • 稳妥构造函数模式
  • 继承
    • 原型链
      • 原型链的问题
        • 借用构造函数
          • 传递参数
        • 组合继承(最常用)
        • 原型式继承
        • 寄生式继承
        • 寄生组合式继承
  • 小结
    • 属性:
    • 创建对象
    • 继承

理解对象

对象的创建方式

  1. var o = new Objec();// 创建Object对象实例
  2. var o = {};// 使用对象字面量

属性类型

这些特征是为了实现JS引擎用的,因此JS中不能直接访问它们,为了表示特征是内部值,使用方括号类似 [[Enumerable]] 的方法表示;两种属性:数据属性、访问器属性

数据属性

包含数据值

4个特性

  • [[Configurable]]:表示能否通过delete删除属性而重新定义属性,能否修改属性的特性,或者能够把属性改为访问器属性;
  • [[Enumberable]]:表示能够通过 for-in 循环返回属性;
  • [[Writable]]:表示能否修改属性的值;
  • [[value]]:包含这个属性的数据值,默认为 undefined;

修改属性

只能通过Object.defineProperty()方法修改属性默认的特性

var person = {};
// person:属性所在的对象
// name:属性名称
// {}:描述符对象 -> 修改属性特性(configurable、enummerable、writable、value)

Object.defineProperty(person,"name",{
    value : "Jason",
});

// 获取person的描述符对象
var decriptor = Object.getOwnPropertyDescriptor(person,"name");

// 特性的默认值
console.log(decriptor.configurable);//false
console.log(decriptor.writable);//false
console.log(decriptor.enumerable);//false
console.log(decriptor.value);//Jason
console.log(person.name);//Jason
// 但我们后面会知道,用户自定以的函数中的 configurable,writable,enumerablede的默认值均为false

如果手动将 configurable 设置为 false,那么不能再重新配置为 true;

访问器属性

不包含数据值,包含一对 getter() 和 setter() 函数

4个特性

  • [[Configurable]]:表示能否通过delete删除属性而重新定义属性,能否修改属性的特性,或者能够把属性改为数据属性;
  • [[Enumberable]]:表示能够通过 for-in 循环返回属性;
  • [[Get]]:读取属性时调用的函数,默认为undefined;
  • [[value]]:写入属性时调用的函数,默认为 undefined;
var book = {
    _year : 2004,
    edition : 1
};

Object.defineProperty(book,"year",{
// 在严格模式下,需要 getter 和 setter 函数一起定义
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if(newValue > 2004){
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
})
// 获取描述符对象
var decriptor = Object.getOwnPropertyDescriptor(book,"year");

// 特性的默认值
console.log(decriptor.configurable);//false
console.log(decriptor.enumerable);//false
console.log(typeof decriptor.get);//function
console.log(typeof decriptor.set);//function

book.year = 2005;
console.log(book.edition);  //2

// 除了Object.defineProperty(),也可以使用 _defineGetter_() 和 _defineSetter_()
// book._defineGetter_("year",function(){})

定义多个属性

Object.defineProperties()

var book = {};
Object.defineProperties(book,{
    _year: {
        value : 2004
    },
    edition: {
        value : 1
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if(newValue > 2004){
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
})

一个属性不能同时具有数据属性和访问器属性特性,下面是错误的示范

// 
var book = {};
Object.defineProperties(book,{
    year: {
        value: 2006,
        get: function(){
            
        }
    }
});

读取属性的特性

Object.getOwnPropertyDescriptor() --> 返回一个对象,前面有对应的语法

创建对象

工厂模式

  1. 创建一个Object接收参数
  2. 给Object添加属性
  3. 返回该Object
  4. 缺点:没有对象名称,不能识别对象;本质上是 object;
function createPerson(name,age,job){
    var o = new Object;
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    }
    return o;
}
var person1 = createPerson("Jason",18,"enginner");
person1.sayName();// Jason
console.log(typeof person1);// object

构造函数模式

特点:

  1. 必须使用 new 操作符 (和其它函数的唯一区别)
  2. 构造函数命名应该 以大写字母开头
  3. 实例对象有一个 constructor 属性指向 Person
  4. 缺点:每个方法都要在每个实例上重演一遍 (在JS中,函数也是对象–Function对象);不同函数中的同名函数是不相等的

创建步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新函数(this指向了这个新对象)
  3. 执行构造函数中的代码(为对象添加新属性)
  4. 返回新对象
function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log(this.name);
    }
    // this.sayName = new Function("console.log(this.name)");
}
var person1 = new Person("Jason",18,"enginner");
var person2 = new Person("Gero",19,"enginner");

person1.sayName();// Jason
console.log(person1 instanceof Person);// true
console.log(person1.constructor == Person);// true
console.log(person1.sayName == person2.sayName);//false

// 为了使对象的方法为同一个对象,可以将sayName定义为外部函数
function sayName(){
    console.log(this.name);
}

this.sayName = sayName;
// 但是对象如果要定义很多方法,那么就要定义很多个全局函数,可以使用原型模式

原型模式

每个函数都有一个 prototype 属性。这个属性是一个指针,指向一个对象;

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

function Person(){
    Person.prototype.name = "Jason";
    Person.prototype.age = 19;
    Person.prototype.job = "enginner";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
    // this.sayName = new Function("console.log(this.name)");
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName == person2.sayName);// true

原型对象

只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,指向函数的原型对象

这个原型对象会自动获得一个 constructor 属性,指向 prototype 属性所在函数的指针(Person.prototype.constructor 指向 Person)

没有标准的方式访问[[ Prototype ]],但是 FireFox、Safari、Chrome 在每个对象上都支持一个属性 proto

需要明确一点的是:这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间

JS--面向对象的程序设计_第1张图片

isPrototypeOf() 方法和 getPrototypeOf() 方法

// 判断person1的[[Prototype]]是否指向Person.prototype
console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(person2));//true

// 另一个方法 getPrototypeOf()方法返回[[Prototype]];ES5新增
console.log(Object.getPrototypeOf(person1) == Person.prototype);// true

属性搜索的顺序

从对象实例本身开始,如果找到给定名称的属性,则返回该属性的值;如果没有,则继续搜索指针指向原型对象。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值

使用 delete() 可以完全删除实例属性,但保留了原型中的值

// 给实例赋值,会屏蔽原型中的值,但不会覆盖
function Person(){
    Person.prototype.name = "Jason";
    Person.prototype.age = 19;
    Person.prototype.job = "enginner";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
    // this.sayName = new Function("console.log(this.name)");
}
// 注意:不能直接Person.sayName() 或 Person.protype.sayName();
var person1 = new Person();
var person2 = new Person();
person1.name = "JJ";
console.log(person1.name);// JJ
console.log(person2.name);// Jason

delete(person1.name);
console.log(person1.name);  // Jason

hasOwnProperty()方法

用于检测一个属性是存在于实例中,还是存在于原型中。只有当属性存在于实例中时,才返回true;

不能用此方法来检测一个属性是否存在于原型中(可以使用 in 操作符 和 hasOwnProperty())

// 给实例赋值,会屏蔽原型中的值,但不会覆盖
function Person(){
    Person.prototype.name = "Jason";
    Person.prototype.age = 19;
    Person.prototype.job = "enginner";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
    // this.sayName = new Function("console.log(this.name)");
}
var person1 = new Person();
console.log(person1.hasOwnProperty("name"));// false --> 来自原型

person1.name = "JJ";
console.log(person1.hasOwnProperty("name"));// true --> 来自实例

原型与 in 操作符

in

in 操作符会在通过对象能够访问给定属性时返回 true。无论是实例中还是原型中。

function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) && (name in object);
}

for-in

for-in:返回的是所有能够通过对象访问的、可枚举的属性,包括存在实例和原型重的属性;

根据规定,所有开发人员定义的属性都是可枚举的;但对于使用 Object.defineProperty()定义的属性来说,enumerable默认为false

var o = {}
o.sayName = function(){console.log("hello")};
var descriptor = Object.getOwnPropertyDescriptor(o,"sayName");
console.log(descriptor.enumerable);// true
for(var prop in o){
    if(prop == "sayName"){
        console.log(prop);// sayName
    }
}
// 使用Object.defineProperty()定义属性
Object.defineProperty(o,"sayHello",{
    value:function(){console.log("hello")}
})
var descriptor = Object.getOwnPropertyDescriptor(o,"sayHello");
console.log(descriptor.enumerable);//false
for(var prop in o){
    if(prop == "sayHello"){
        consoloe.log(prop);// 此行不会被打印
    }
}

Objecy.keys()

返回一个包含所有可枚举属性的字符串数组

function Person(){
    Person.prototype.name = "Jason";
    Person.prototype.age = 19;
    Person.prototype.job = "enginner";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}
Person();// 在 Chrome 上不写这句或者没有调用过 Person() 输出的 keys 为空
var keys = Object.keys(Person.prototype);
console.log(keys);// ["name", "age", "job", "sayName"]

Object.getOwnPropertyNames()

获取所有实例的属性,包括不可枚举的属性

console.log(Object.getOwnPropertyNames(Person));
// ["constructor", "name", "age", "job", "sayName"]

更简单的原型语法

我们可以使用对象字面量重写原型对象

function Person(){
    Person.prototype = {
        name : "Jason";
        age : 19;
        job : "enginner";
        sayName : function(){
            console.log(this.name);
        }
    }
}
  • 注意:此时我们会发现少了 constructor 这个属性,因为我们完全重写了原型对象;
  • 因此通过 constructor 已经无法确定对象的类型了
  • 但使用 instanceof 操作符仍能返回正确的结果
function Person(){}
// 注意是在函数的外部定义,内部定义则直接定义 prototype
Person.prototype = {
    name : "Jason",
    age : 19,
    job : "enginner",
    sayName : function(){
        console.log(this.name);
    }
}
var friend = new Person();
console.log(friend instanceof Object);// true
console.log(friend instanceof Person);// true
console.log(friend.constructor == Person);// false
console.log(friend.constructor == Object);// true

// 如果 constructor 的值真的很重要,可以在 prototype 中添加该值
Person.prototype = {
    constructor : Person,
    //...省略
}
  • 书上说,以这种方式重设 constructor 属性会导致 [[ Enumerable ]] 特性设置为 true
  • 但实际好像不会
function Person(){}
Person.prototype = {
        constructor : Person,
        name : "Jason",
        age : 19,
        job : "enginner",
        sayName : function(){
            console.log(this.name);
        }
    }
var descriptor = Object.getOwnPropertyDescriptor(Person.prototype,"constructor");
console.log(descriptor.enumerable);// false
for(var prop in Person.prototype){
    if(prop == "constructor")
        console.log(prop);// 此行不会被输出
}

原型的动态性

我们对原型对象所做的任何修改都能立即从实例上反映出来,即使是先创建实例后修改原型。

var friends = new Person();
Person.prototype.sayHi = function(){
    console.log("Hi!");
}
friends.sayHi();// Hi!
  • 这和实例与原型之间的松散连接关系有关;实例和原型之间的连接只不过是一个指针,而非一个副本;
  • 但是如果重写原型对象,那么将不会反映到重写之前的实例上;
Person.prototype = {
    constructor : Person,
    sayHi : function(){
        console.log("hi");
    }
}
friends.sayHi();
var friends = new Person();
friends2.sayHi();

// 此时的 friends 和 friends2 指向不同的原型对象

原生对象的原型

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新的方法;

console.log(typeof Array.prototype.sort);// function
console.log(typeof String.prototype.substring);// function

String.prototype.startsWith = function(text){
    return this.indexOf(text) == 0;
}
var msg = "Hello world";
console.log(msg.startsWith(msg));// true
  • 但不推荐在产品化的过程中修改原生对象的原型;

原型对象的问题

  1. 省略了为构造函数传递初始化参数这一环节;
  2. 最大的问题是:原型中的属性和方法都是共享的;
  • 对于基本类型来说,我们可以直接在实例上添加属性即可
  • 但是对于引用类型就会存在问题
function Person(){}
Person.prototype = {
        constructor : Person,
        name : "Jason",
        age : 19,
        job : "enginner",
        friends:["Shelby","Court"],
        sayName : function(){
            console.log(this.name);
        }
    }
var person1 = new Person();
var person2 = new Person();
person1.friends.push("van");
console.log(person2.friends);
// ["Shelby", "Court", "van"]
console.log(person1.friends == person2.friends);// true
  • 因为 person1 实例上没有 friends 属性,所以会从原型中找,由于 friends 是引用类型,所以我们修改它的值会直接影响原型对象;
  • 实例一般是要有属于自己的全部属性的;

构造函数模式 + 原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性;

function Person(name,age,job){
    this.name = name,
    this.age = age,
    this.job = job,
    this.friends = []
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        console.log(this.name);
    }
}
var person1 = new Person("Jason",18,"enginner");
var person2 = new Person("Tom",19,"enginner");
person1.friends.push("JJ");
person2.friends.push("Tony");
console.log(person1.friends);// ["JJ"]
console.log(person2.friends);// ["Tony"]
console.log(person1.sayName == person2.sayName);// true

动态原型模式

将所有的信息都封装在了构造函数中,通过构造函数中初始化原型(仅在必要的情况下)

可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = [];
    // 只有在sayName()方法不存在的情况下,才会添加到原型中
    if(typeof this.sayName != "function"){
        Person.prototype = function(){
            console.log(this.name);
        }
    }
}
var person1 = new Person("Jason",18,"enginner");
person1.sayName();
  • 不能使用对象字面量重写原型,因为会切断实例与新原型之间的联系;(不能使用this关键字)

寄生构造函数模式

基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象;

// 这其实和使用工厂模式创建对象方式是一样的
function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    }
    return o;
}
  • 这个模式在特殊的情况下用来为对象创建构造函数;例如我们希望创建一个具有额外方法的特殊数组,但我们并不能直接修改Array构造函数,因此可以使用这个模式;
function SpcialArray(){
    var values = new Array();
    
    // 添加值
    values.push.apply(values,arguments);
    
    // 添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    
    return values;
}

var colors = SpcialArray("red","blue","green");
console.log(colors.toPipedString());// red|blue|green
  • 返回的对象与构造函数或者与构造函数的原型属性之间是没有关系的;(我们甚至可以不使用new关键字)
  • 因此我们不能依赖 instanceof 操作符来确定对象类型
console.log(typeof colors);
console.log(colors instanceof Array);// true
console.log(colors instanceof SpcialArray);// false

稳妥构造函数模式

所谓的稳妥对象字指的是没有公共属性,而且其方法也不引用 this 的对象

所以适合在一些安全的环境中(禁用this和new的环境),或者防止数据被其它应用程序改动时使用。

  • 与寄生构造函数模式不同的点:
  1. 新创建对象的实例不引用this
  2. 不使用new操作符调用构造函数
function Person(name,age,job){
    var o = new Object();
    //此处定义私有变量和函数
    o.sayName : function(){
        console.log(name);// 此处不是this.name
    }
    return o;
}
var person1 = Person("Jason",19,"enginner");
console.log(person1.name);// undefined
person1.sayName();// Jason
  • 除了调用sayName()方法访问成员变量外,没有别的方式;

继承

继承的方式:接口继承(只继承方法签名) 和 实现继承(继承实际的方法)

由于函数没有签名,所以ECMAScript只支持实现继承

原型链

ECMAScript 描述了原型链的概念,并将原型链作为实现继承的主要方法;

  • 回顾:构造函数、原型和实例的关系

每个构造函数都有一个原型对象 (xxx.prototype),原型对象都包含一个指向构造函数的指针(xxx.prototype.constructor),而实例都包含一个指向原型对象的内部指针 [[Prototype]] ;

  • 我们让原型对象等于另一个类型的实例看会发生什么:
function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subProperty = false;
}

// 继承了 SuperType --> 具有了 SuperType的所有属性和方法
// 注意事项:
// 1. 此时的constructor不再指向SubType(重写了原型对象),而是指向了SuperType
// 2. 给原型添加方法的代码一定要放在替换原型的语句之后
// 3. 不能使用对象字面量创建原型方法
SubType.prototype = new SuperType();

// SubType.prototype = {}  错误示范,会重写原型对象
SubType.prototype.getSubValue = function(){
    return this.subProperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());// true
  • 实现的本质是重写原型对象
  • getSuperValue()的搜索过程
  1. 搜索实例
  2. 搜索 SubType.prototype
  3. 搜索 SuperType.prototype
  4. 实际上会继续搜索 Object.prototype --> 所有的引用类型默认都继承了Object
  • 确定原型和实例的关系
console.log(instance instanceof Object);// true
console.log(instance instanceof SuperType);// true
console.log(instance instanceof SubType);// true

// 只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型
console.log(Object.prototype.isPrototype(instance));// true
console.log(SuperType.prototype.isPrototype(instance));// true
console.log(subType.prototype.isPrototype(instance));// true
  • 谨慎定义方法

给原型添加方法的代码一定要放在替换原型的语句之后

不能使用对象字面量创建原型方法(因为这样重写了原型,将 SuperType 的实例作为 SubType 的原型对象也就没有意义了)

原型链的问题

  1. 主要的问题来自包含引用类型值的原型;
function SuperType(){
    this.colors = ["red","blue","green"];
}

function SubType(){
    
}

// 此时的所有SubType都共用一个原型对象,即也共用一个 colors
SubType.prototype = new SuperType();

var instance = new SubType();
instance.colors.push("black");
console.log(instance.colors);   //["red", "blue", "green", "black"]

var instance1 = new SubType();
console.log(instance1.colors);  //["red", "blue", "green", "black"]
  1. 在创建子类型的实例时,不能向超类型的构造函数中传递参数。应该说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

所以实际上很少会单独使用原型链。

借用构造函数

有时也叫伪造对象或经典继承;

function SuperType(){
    this.colors = ["red","blue","green"];
}

function SubType(){
    // 继承了 SuperType --> 借调了操类型的构造函数
    SuperType.call(this);   // 使用apply()也可以
}

var instance = new SubType();
instance.colors.push("black");
console.log(instance.colors);   //["red", "blue", "green", "black"]

var instance1 = new SubType();
console.log(instance1.colors);  //["red", "blue", "green"]
传递参数

接用构造函数的很大一个优势是可以在子类型构造函数中向超类型构造函数传递参数;

function SuperType(name){
    this.name = name;
}
Supertype.prototype.sayHi = function(){
    console.log("Hi!")    
}

function SubType(){
    SuperType.call(this,"Jason");
    this.age = 29;
}

var insatance = new SubType();
console.log(instance.name);// 29
console.log(instance.age );// Jason
instance.sayHi();   // 此时会报错。子类型无法访问到父类型的原型

无法避免构造函数模式存在的问题:方法都在构造函数中定义,因此函数复用就无从谈起。

在超类型的原型中定义的防范,对子类型而言也是不可见的(子类型无法访问到父类型的原型),结果所有类型都只能使用构造函数模式;

组合继承(最常用)

也叫伪经典继承,指的是将原型链和接用构造函数的技术组合在一块。即使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

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

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var instance1 = new SubType("Jason",29);
instance1.colors.push("black");
console.log(instance1.colors);// ["red", "blue", "green", "black"]
instance1.sayName();// Jason
instance1.sayAge();// 29

var instance2 = new SubType("Greg",27);
console.log(instance2.colors);// ["red", "blue", "green"]
instance2.sayName();// Greg
instance2.sayAge();// 27
  • 即可以让两个不同的 SubType 实例分别拥有自己的属性—包括 colors 属性,又可以使用相同的方法;

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型;

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}
var person = {
    name : "Jason",
    friends:["Shelby","Court","Van"]
};

// 相当于对 person 的浅复制(只针对引用类型)
var anotherPerson = object(person);
anotherPerson.name = "Tom";
anotherPerson.friends.push("Gerg");
console.log(person.friends);    // ["Shelby", "Court", "Van", "Gerg"]
console.log(person.name);   // Jason

要求必须有一个对象可以作为另一个对象的基础;

此处的friends不仅归person所有,也归anotherPerson所有

Object.create()

和object()方法的行为相同;ES5

var person = {
    name : "Jason",
    friends:["Shelby","Court","Van"]
};

// 相当于对 person 的浅复制(只针对引用类型)
var anotherPerson = Object.create(person);
anotherPerson.name = "Tom";
anotherPerson.friends.push("Gerg");
console.log(person.friends);    // ["Shelby", "Court", "Van", "Gerg"]
console.log(person.name);   // Jason

// Object.create()可以接收第二个参数,和Object.defineProperties()方法的第二个参数格式相同
var yetAnotherPerson = Object.create(person,{
    name : {
        value : "Tom"
    },
    friends: {
        value : ["Lucy","Tony"]
    }
});
console.log(person.friends);// ["Shelby", "Court", "Van", "Gerg"]
console.log(yetAnotherPerson.friends);// ["Lucy", "Tony"] --> 此时只是修改了 yetAnotherPerson的friends
  • 以这种方式指定的任何属性都会覆盖原型对象上的同名属性

寄生式继承

function createAnother(original){   
    //  此时的object不是必须的,可以直接在original上添加方法
    var clone = object(original);
    clone.sayHi = function(){
        console.log("hi");
    }
    return clone;
}

var person = {
    name: "Nicholas",
    friends: ["Lucy","Tony"]
    
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();

使用寄生式继承来为对象添加函数,不能做到函数的复用,这一点和构造函数模式类似

寄生组合式继承

组合继承最大的问题是无论什么情况下,都会调用两次超类型构造函数

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型函数的混成形式来继承方法。

  • 回顾组合继承
function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name,age){
    SuperType.call(this,name);// 第二次调用SuperType()
    this.age = age;
}

SubType.prototype = new SuperType();// 第一次调用SuperType()
SubType.prototype.sayAge = function(){
    console.log(this.age);
}
  • 使用寄生组合继承方式
// 其实就是直接操作了SuperType的原型对象,而不通过构造函数
function inheritPrototype(superType,subTypr){
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

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

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name,age){
    SuperType.call(this,name);  // 只调用了一次SuperType的构造函数
    this.age = age;
}

inheritPrototype(SuperType,SubType);

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

开发人员普遍认为寄生组合继承是引用类型最理想的继承方式;

小结

属性:

  • 数据属性:
    • Configurable,Enumerable,Writable,Value
    • 如果使用Object.defineProperty()定义的属性,configurable,enumerable默认为false
  • 访问器属性
    • Configurable,Enumerable,Set,Get
    • 如果使用Object.defineProperty()定义的属性,configurable,enumerable默认为false
  • 定义多个属性:Object.defineProperties(object,propertyName,{});
  • 读取属性特性:Object.getOwnPropertyDescriptor(object); -> 获取一个对象,包含数据属性或访问器属性

创建对象

  • 工厂模式
function createPerson(){
    var o = new Object();
    o.name = "Jason";
    return o;
}
  • 构造函数模式
function Person(name,age){
    this.name = name;
    this.age = age;
}
var person = new Person();
  • 原型模式
  • 构造函数+原型模式
function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        console.log(this.name);
    }
}
var person = new Person("Jason",18);
person.sayName();
  • 动态原型模式

我们在创建实例对象后修改原型对象将会立即放映到实例上(不能使用对象字面量)

Person.prototype.sayHi = function(){
    console.log("Hi!" + this.name);
};
person.sayHi();
function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = [];
    // 只有在sayName()方法不存在的情况下,才会添加到原型中
    // 在第一次调用构造方法时执行,只执行一次
    // 这样做的好处仅仅是在构造函数中就封装好原型对象吧。。
    if(typeof this.sayName != "function"){
        Person.prototype = function(){
            console.log(this.name);
        }
    }
}
var person1 = new Person("Jason",18,"enginner");
person1.sayName();
  • 寄生构造函数模式
// 寄生在原有的引用类型上,并对其添加属性或方法
function SpcialArray(){
    var values = new Array();
    
    // 添加值
    values.push.apply(values,arguments);
    
    // 添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    
    return values;
}

var colors = new SpcialArray("red","blue","green");
console.log(colors.toPipedString());// red|blue|green
console.log(colors instanceof Array);// true
console.log(colors instanceof SpcialArray);// false -> 并没有通过SpcialArray的构造函数
  • 稳妥构造函数模式
// 此模式安全,不使用this和new关键字
// 只能通过调用函数内部的方法来访问成员变量(值)
function Person(name,age,job){
    var o = new Object();
    //此处定义私有变量和函数    (外部只能通过该方法访问name)
    o.sayName : function(){
        console.log(name);// 此处不是this.name
    }
    return o;
}
var person1 = Person("Jason",19,"enginner");
console.log(person1.name);// undefined
person1.sayName();// Jason

继承

  • 原型链继承
// 父类型
function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

// 子类型
function SubType(){
    this.subProperty = false;
}

// 子类型的原型指向新的父类型实例
SubType.prototype = new SuperType();

// SubType.prototype = {}  错误示范,会重写原型对象
SubType.prototype.getSubValue = function(){
    return this.subProperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());// true
  • 借用构造函数

使用()call/apply()

function SuperType(){
    // 此处的colors不是实例共享的属性(有this)
    this.colors = ["red","blue","green"];
}

function SubType(){
    // 解决了向父类型构造函数传递参数的问题
    // 继承了 SuperType --> 借调了操类型的构造函数
    SuperType.call(this);   // 使用apply()也可以
}

var instance = new SubType();
instance.colors.push("black");
console.log(instance.colors);   //["red", "blue", "green", "black"]

var instance1 = new SubType();
console.log(instance1.colors);  //["red", "blue", "green"]
  • 组合继承(最常用)

主要是针对父子类型中的引用类型;(两次调用父类型的构造函数)

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

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

// 如果使用借用构造函数来实现。子类型将不能访问到该方法
SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name,age){
    SuperType.call(this,name);  // 第一次调用 -> 通过借用构造函数来实现对实例属性的继承
    this.age = age;
}

SubType.prototype = new SuperType();    // 第二次调用 -> 使用原型链实现对原型属性和方法(sayName)的继承
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var instance1 = new SubType("Jason",29);
instance1.colors.push("black");
console.log(instance1.colors);// ["red", "blue", "green", "black"]
instance1.sayName();// Jason
instance1.sayAge();// 29

var instance2 = new SubType("Greg",27);
console.log(instance2.colors);// ["red", "blue", "green"]
instance2.sayName();// Greg
instance2.sayAge();// 27
  • 原型式继承
// 浅复制;ES5的Object.create()和该方法类似; -> 只针对引用类型
// 理解基本类型和引用类型作为参数传递时的区别即可
function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}
var person = {
    name : "Jason",
    friends:["Shelby","Court","Van"]
};

// 相当于对 person 的浅复制(只针对引用类型)
var anotherPerson = object(person);
anotherPerson.name = "Tom";
anotherPerson.friends.push("Gerg");
console.log(person.friends);    // ["Shelby", "Court", "Van", "Gerg"]
console.log(person.name);   // Jason --> 基本类型不变
  • 寄生式继承
function createAnother(original){   
    //  此时的object不是必须的,可以直接在original添加方法(达到复用?)
    var clone = object(original);
    clone.sayHi = function(){
        console.log("hi");
    }
    return clone;
}

var person = {
    name: "Nicholas",
    friends: ["Lucy","Tony"]
    
};

var anotherPerson = createAnother(person); // 寄生在person实例上添加了sayHi()方法
anotherPerson.sayHi();
  • 寄生组合继承方式

只调用了一次父类型的构造函数

// 其实就是直接操作了SuperType的原型对象,而不通过构造函数(可能有点绕)
function object(o){
    function f(){};
    f.prototype = o;
    // 注意是new,而不是直接返回啊。。。
    return new f();
}
function inheritPrototype(superType,subType){
    //superType.prototype.constructor = subType;
    //subType.prototype = superType.prototype;
    // 或直接使用 Object.create()
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

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

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name,age){
    SuperType.call(this,name);  // 只调用了一次SuperType的构造函数
    this.age = age;
}

inheritPrototype(SuperType,SubType);    // SubType.prototype = new SuperType();

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var subType = new SubType("Jason",19);
subType.sayName();

你可能感兴趣的:(web前端,js面向对象,js继承)