第六章 面向对象的程序设计

第六章 面向对象的程序设计

  • 每个对象都是基于一个引用类型创建的,可以是第五章的原生类型,也可以是开发人员定义的类型

6.1 理解对象

//早期
var person = new Object();
person.name = "Kevin";
person.age = 29;
person.say = function(){
    console.log(this).name;
}
//升级
var person = {
    name : "Kevin",
    age : 29,
    say : function(){
        console.log(this).name;
    }
};

6.1.1 属性类型

数据属性和访问器属性

数据属性
特性 说明
[[Configurable]] 是否可配置(支持delete 能否修改属性特性 能否修改为访问器属性) 默认值true
[[Enumerable]] 是否可for-in循环返回 默认为true
[[Writable]] 可修改 默认为true
[[Value]] 属性的数据值 默认为undefined
var person = {
    name: "Kevin"
}
//name的 [[Configurable]] [[Enumerable]] [[Writable]] 为true
//name的 [[Value]] 为 "Kevin"
  • 修改方法:使用ES5的 Object.defineProperty()方法
    • 接收三个参数:对象 属性名 配置
    • 如果不指定 [[Configurable]] [[Enumerable]] [[Writable]] 默认为false
var person  = {};
Object.defineProperty(person, "name", {
    writable: false,
    configurable: false, //一旦定义不可配置 就不可变回可配置
    value: "Kevin"
})
console.log(person.name); //Kevin

person.name = "Tom";
delete person.name;
console.log(person.name); //Kevin 严格模式会报错
  • 理解会非常有用
访问器属性
特性 说明
[[Configurable]] 是否可配置(支持delete 能否修改属性特性 能否修改为访问器属性) 默认值true
[[Enumerable]] 是否可for-in循环返回 默认为true
[[Get]] 读取访问器属性时调用 默认为undefined
[[Set]] 写入访问器属性时调用 默认为undefined
  • 只指定get意味着不能写 只指定set意味着不能读
var book = {
    _year: 2019,//年份
    edition: 1 //版本
};
Object.defineProperty(book, "year", {
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if(newValue > 2019){
            this._year = newValue;
            this.edition = (newValue - 2019) + 1;
        }
    }
})
book.year = 2020;
console.log(book.edition); //2
//旧有的非标准方法
var book = {
    _year: 2019,//年份
    edition: 1 //版本
};
book.__defineGetter__("year",function(){
    return this._year;
});
book.__defineSetter__("year",function(newValue){
    if(newValue > 2019){
        this._year = newValue;
        this.edition = (newValue - 2019) + 1;
    }
});
book.year = 2020;
console.log(book.edition); //2

6.1.2 定义多个属性

IE9+

var book = {};
Object.defineProperties(book, 
    {
        _year: {
            writable: true,
            configurable: true,
            value: 2019
        },
        edition: {
            writable: true,
            configurable: true,
            value: 1
        },
        year: {
            get: function(){
                return this._year;
            },
            set: function(newValue){
                if(newValue > 2019){
                    this._year = newValue;
                    this.edition = (newValue - 2019) + 1;
                }
            }
        }
    }
)
book.year = 2020;
console.log(book.edition); //2

6.1.3 读取属性的特性

getOwnPropertyDescriptor

var desc = Object.getOwnPropertyDescriptor(book, "_year");
console.log(desc.value); //2020
console.log(desc.configurable); //true
console.log(desc.get); //undefined
  • 在JS中,可以针对任何对象--包括DOM和BOM对象,使用Object.getOwnPropertyDescriptor()

6.2 创建对象

6.2.1 工厂模式

function createPerson(name, age){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function(){
        console.log(this.name);
    }
    return o;
}
var person1 = createPerson('Tom',23);
var person2 = createPerson('Kevin',29);
  • 缺点:无法知道对象的类型

6.2.2 构造函数模式

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        console.log(this.name);
    }
}
var person1 = new Person('Tom',23);
var person2 = new Person('Kevin',29);
  • 区别
    • 没有显示创建对象
    • 属性方法赋给this
    • 没有return
    • 函数名首字母大写
//检测对象类型constructor
console.log(person1.constructor == Person); //true
//检测对象类型instanceof 推荐
console.log(person1 instanceof Person); //true
console.log(person1 instanceof Object); //true
  • 胜过工厂模式地方:实例可以标识为一种特定的类型
将构造函数当作函数
//当作构造函数
var person1 = new Person('Tom',23);
person1.sayName(); //Tom

//当作普通函数
Person('Bob',26);
window.sayName(); //Bob

//在另一个作用域中调用
var o = new Object();
Person.call(o, "Kevin", 29);
o.sayName();
  • 缺点 每个方法都要在每个实例上面重新创建一遍
function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = new Function("console.log(this.name)");
}
//改进方法: 将通用方法提取仅保留指针 但这样该方法属于全局,封装性差
function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name);
}

6.2.3 原型模式

  • 创建的每个函数都有一个prototype原型属性,是指针,指向一个对象。这个对象的用途是包含特定类型的所有实例 共享的属性和方法。
function Person(){
    
}
Person.prototype.name = "Kevin";
Person.prototype.age = 29;
Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();
person1.sayName(); //Kevin
var person2 = new Person();
person2.sayName(); //Kevin
console.log(person1.sayName == person2.sayName);
理解原型对象

[拓展]6.2.3 原型对象

原型与in操作符 (检测实例中或原型中是否含有)
function Person(){
    
}
Person.prototype.name = "Kevin";
Person.prototype.age = 29;
Person.prototype.sayName = function(){
    console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); //true

person1.name = "Kevin";
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
//判断存在对象原型中还是对象中
function hasPrototypeNotProperty(object, name){
    return !object.hasOwnProperty("name") && (name in object);    
}
  • for-in 获取实例中和原型中的属性
function Person(){
    
}
Person.prototype.name = "Kevin";
Person.prototype.age = 29;
Person.prototype.sayName = function(){
    console.log(this.name);
}
var person1 = new Person();
for(var prop in person1){
    console.log(prop); // name age sayName
}
  • Object.keys() 获取所有可枚的实例属性
var keys = Object.keys(Person.prototype);
console.log(keys); //["name", "age", "sayName"]

var p1 = new Person();
p1.name = "Bob";
p1.age = 26;
var p1keys = Object.keys(p1);
console.log(p1keys); //["name", "age"]
  • Objec.getOwnPropertyNames() 获取可枚举和不可枚举的实例属性
var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); //["constructor", "name", "age", "sayName"]
更简单的原型语法
function Person(){
}
Person.prototype = {
    name: "Kevin",
    age: 29,
    sayName: function(){
        console.log(this.name);
    }
}
  • 虽然视觉上更好的封装了原型 不过相当于重写了Person.prototype
    此时Person.prototype.constructor不再指向Person 而是指向Object构造函数
    此时instanceof 可以返回正确结果 而constructor无法确定对象类型
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
function Person(){
}
Person.prototype = {
    constructor: Person,
    name: "Kevin",
    age: 29,
    sayName: function(){
        console.log(this.name);
    }
}
  • 但手工设定时 默认是可枚举的,所以可以使用Object.defineProperty()
function Person(){
}
Person.prototype = {
    name: "Kevin",
    age: 29,
    sayName: function(){
        console.log(this.name);
    }
}
Object.defineProperty(Person.prototype, "constructor",{
    enumerable: false,
    value: Person
})
原型的动态性
原生对象的原型
  • 原生模式的重要性不仅体现在创建自定义类型 还有所有原生的引用类型,都是采用这种模式创建的
  • Object Array String...
console.log(typeof Array.prototype.sort); //function
String.prototype.startsWith = function(text){
    return this.indexOf(text) == 0;
}
var msg = "Hello world";
console.log(msg.startsWith("Hello")); //true

不推荐定义原型新方法 防止多环境同名覆盖

原型对象的问题
  • 省略了为构造函数传递初始化参数环节
  • *包含引用类型值属性时 原型的共享属性问题
function Person(){}
Person.prototype = {
    constructor: Person,
    name: "Kevin",
    age: 29,
    friends: ["Bob","Sily"]
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); //["Bob","Sily","Van"]
console.log(person2.friends); //["Bob","Sily","Van"]
console.log(person1.friends === person2.friends); //true

6.2.4 组合使用构造函数模式和原型模式

  • ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法
  • 实例属性在构造函数中定义 实例共享属性constructor和方法在原型中定义
function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Van"];
}
Person.prototype = {
    constructor: Person,
    sayName: function(){
        console.log(this.name);
    }
}
var person1 = new Person("Kevin", 29, "Software Engineer");
var person2 = new Person("Greg", 23, "Doctor");
person1.friends.push("Lily");
console.log(person1.friends); //["Van","Lily"]
console.log(person2.friends); //["Van"]

6.2.5 动态原型模式

  • 让构造函数和原型不再独立的好解决方案
  • 通过检查某个存在的方法是否有效(不用全部)来决定是否需要初始化原型
  • 只在初次调用构造函数时执行
  • 不能使用字面量重写原型 保持constructor指向正确的Person
function Person(name, age, job){
    //属性
    this.name = name;
    this.age = age;
    this.job = job;
    
    //方法
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        }
    }
}
var friend = new Person("Kevin", 29, "Software Engineer");
friend.sayName(); //Kevin

6.2.6 寄生构造函数模式

  • 不推荐使用
  • 返回的对象与构造函数或构造函数原型属性之间没有关系
function createPerson(name, age){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function(){
        console.log(this.name);
    }
    return o;
}
var person1 = new createPerson('Tom',23); //和工厂模式一模一样 不同处是使用new实例化
  • 具有额外方法的特殊数组
function SpecialArray(){
    var spArray = new Array();
    spArray.push.apply(spArray, arguments);
    spArray.toPopedString = function(){
        return this.join("|");
    }
    return spArray;
}
var colors = new SpecialArray("red", "yellow", "green", "blue");
console.log(colors.toPopedString()); //red|yellow|green|blue

6.2.7 稳妥构造函数模式

  • 禁用使用this和new
  • 适合在一些安全的环境中(ADSafe)
  • 返回的对象与构造函数或构造函数原型属性之间没有关系
function Person(name, age, job){
    var o = new Object();
    o.sayName = function(){
        console.log(name);
    }
    return o;
}
var friend = Person("Kevin", 29, "Engineer");
friend.sayName(); //Kevin

6.3 继承

  • 继承是面向对象语言的强大概念。所有OO语言支持两种继承方式:接口继承和实现继承
  • 接口继承只继承方法签名,而实现继承则继承实际的方法
  • 由于函数没有签名,所以ECMAScript中无法实现接口继承,只支持实现继承(由原型链实现)

6.3.1 原型链

  • 利用原型让一个引用类型继承另一个引用类型的属性和方法
  • A送一个实例a给B,B的prototype置为a,则B可以通过a使用到A原型方法。相当于B继承了A
function SuperType(){
    this.property = "super";
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subproperty = "sub";
}
//继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
    return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue()); //super

console.log(instance.constructor == SuperType); //true
console.log(instance.constructor == SubType); //false
  • 所有函数的默认原型是Object的实例

  • 默认原型会包含一个内部指针 指向Object.prototype

  • [拓展] 6.3.1 原型链

确定原型和实例的关系
  • instanceof
console.log(instance instanceof	Object); //true
console.log(instance instanceof	SubType); //true
console.log(instance instanceof	SuperType); //true
  • isPrototypeOf()
console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true
谨慎地定义方法
  • 重写超类型中的方法
function SuperType(){
    this.property = "super";
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = "sub";
}
//继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSuperValue = function(){
    return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue()); //sub
  • 重写超类型方法,需要在实例需要替换了原型后,即继承好了后定义
  • 实现继承时,不能使用对象字面量创建原型方法,这样会覆盖之前的继承导致错误
原型链的问题
  • 引用类型值的问题
  • 不能向超类型的构造函数传递参数
function SuperType(){
    this.colors = ["red", "yellow", "blue"];
}
function SubType(){
}
//继承SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("green");// ["red", "yellow", "blue", "green"]

var instance2 = new SubType();
console.log(instance2.colors); // ["red", "yellow", "blue", "green"]

6.3.2 借用构造函数

  • 解决原型中包含引用类型值共享问题
  • 也叫伪造对象或经典继承
  • 基本思想特别简单:在子类型构造函数内部调用超类型构造函数
function SuperType(){
    this.colors = ["red", "yellow", "blue"];
}
function SubType(){
    //继承SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("green");// ["red", "yellow", "blue", "green"]
console.log(instance1.colors); // ["red", "yellow", "blue", "green"]
var instance2 = new SubType();
console.log(instance2.colors); // ["red", "yellow", "blue"]
  • 相对于原型链的好处
    • 可以传递参数
function SuperType(name){
    this.name = name;
}
function SubType(){
    //继承SuperType
    SuperType.call(this, "Kevin");
    //实例属性写在继承后 防止覆盖
    this.age = 29;
}
var instance = new SubType();
console.log(instance.name); //Kevin
console.log(instance.age); //29
  • 局限性
    • 方法都是在构造函数中定义 无法复用函数
    • 在超类型的原型中定义的方法,对子类型而言是不可见的
    • 很少单独使用

6.3.3 组合继承

  • 也叫做伪经典继承 是JS中最常用的继承模式
  • 结合原型链和借用构造函数 使用原型链实现对原型属性和方法的继承 使用借用构造函数实现对实例属性的继承
  • 这样,既可通过原型上定义的方法实现函数复用 又保证每个实例有自己的属性
function SuperType(name){
    this.name = name;
    this.color = ["red", "yellow", "blue"];
}
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);
}

6.3.4 原型式继承

  • 借助原型可以基于已有的对象创建新对象 还不必因此创建自定义类型
function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}
  • 可以实现浅复制
var person = {
    name: "Kevin",
    friends: ["Lily","Bob"]
};
var antherPerson = object(person);
antherPerson.name = "Seven";
antherPerson.friends.push("Zoo");

console.log(person.friends); //["Lily","Bob","Zoo"]
  • ES5新增了Object.create()方法规范原型式继承
    • 接收两个参数:原型对象、属性定义对象(可选)
var person = {
    name: "Kevin",
    friends: ["Lily","Bob"]
};
var antherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
})
console.log(antherPerson.name); //Greg
  • 用于在没必要兴师动众创建构造函数 而是想让一个对象与另一个对象相似的情况

6.3.5 寄生式继承

function createAnther(original){
    var clone = Object.create(original); //通过调用函数创建一个新对象 不一定是Object.create
    clone.sayHai = function(){           //以某种方式增强对象
        console.log("Hi");
    }
    return clone;                        //返回这个对象
}
var person = {
    name: "Kevin",
    friends: ["Lily","Bob"]
};
var antherPerson = createAnther(person);
antherPerson.sayHai(); //Hi
  • 不足:为对象添加的函数,不能做到函数复用

6.3.6 寄生组合式继承

  • 业内普遍认为引用类型最理想的继承方式
  • 组合继承最大问题:会调用两次超类型构造函数
  • 借用构造函数继承属性 通过原型链的混成模式继承方法
  • 使用寄生式继承来继承超类型的原型 再将结果指定给子类型的原型
function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}
function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}
function SuperType(name){
    this.name = name;
    this.color = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
    console.log(this.name);
};

function SubType(name, age){
    //继承属性
    SuperType.call(this, name); //第二次调用SuperType()
    this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
    console.log(this.age);
}
posted @ 2019-10-08 17:50  KevinTseng  阅读(103)  评论(0编辑  收藏  举报