对象创建与继承

177 阅读5分钟

1、对象创建

1.1、工厂模式

function createPerson(name, age, job) { 
    let o = new Object();
    o.name = name;
    o.age = age; 
    o.job = job; 
    o.sayName = function() {
        console.log(this.name); 
    };
    return o; 
} 
let person1 = createPerson("Nicholas", 29, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor");

这种工厂模式虽 然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

1.2、构造函数模式

function Person(name, age, job){ 
    this.name = name; 
    this.age = age;
    this.job = job; 
    this.sayName = function() { 
    console.log(this.name); 
}; 
} 
let person1 = new Person("Nicholas", 29, "Software Engineer"); 
let person2 = new Person("Greg", 27, "Doctor"); 
person1.sayName(); // Nicholas
person2.sayName(); // Greg

与工厂模式的区别:

  • 没有显式地创建对象
  • 属性和方法直接赋值给了 this
  • 没有 return

构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例。我们知道,ECMAScript 中的函数是对象,因此每次定义函数时,都会初始化一个对象。如:

function Person(name, age, job){ 
    this.name = name; 
    this.age = age;
    this.job = job; 
    this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}

这样理解这个构造函数可以更清楚地知道,每个 Person 实例都会有自己的 Function 实例用于显 示 name 属性。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新 Function 实例的机制是一样的。因此不同实例上的函数虽然同名却不相等,如下所示:

console.log(person1.sayName == person2.sayName); // false

1.3、原型模式

function Person() {} 

Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
    console.log(this.name); 
}; 

let person1 = new Person();
person1.sayName(); // "Nicholas" 

let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true

一 般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。所以要使用接下来要讲的继承。

2、继承

2.1、 盗用构造函数

function SuperType() { 
    this.colors = ["red", "blue", "green"];
} 
function SubType() { 
    // 继承
    SuperType SuperType.call(this); 
} 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green"

盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模 式。由于存在这些问题,盗用构造函数基本上也不能单独使用。

2.2、组合继承

基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方 法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

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; 
}

// 继承方法,这里主要是利用的new的特性来进行继承的,new时会将构造函数的原型上的方法复制给等号左边的变量
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
}; 

let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);// "red,blue,green,black" 
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); // "red,blue,green" 
instance2.sayName(); // "Greg"; 
instance2.sayAge(); // 27

组合继承弥补了原型链和盗用构造函数的不足,是JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof操作符和 isPrototypeOf()方法识别合成对象的能力。

2.3、原型式继承

object()源码

function object(o) { 
    function F() {} 
    F.prototype = o;
    return new F(); 
}

原型式继承适用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。 你需要把这个对象先传给 object(),然后再对返回的对象进行适当修改。在这个例子中,person 对 象定义了另一个对象也应该共享的信息,把它传给 object()之后会返回一个新对象。这个新对象的原型 是 person,意味着它的原型上既有原始值属性又有引用值属性。这也意味着 person.friends 不仅是 person 的属性,也会跟 anotherPerson 和 yetAnotherPerson 共享。这里实际上克隆了两个 person。

ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。这个方法接收两个 参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时, Object.create()与这里的 object()方法效果相同:

let person = { 
    name: "Nicholas", friends: ["Shelby", "Court", "Van"] 
}; 

let anotherPerson = Object.create(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

Object.create()的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增 属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。比如:

let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"] }; 
let anotherPerson = Object.create(person, { 
name: { 
value: "Greg" 
} 
});
console.log(anotherPerson.name); // "Greg"

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住, 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

2.4、寄生式继承

寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种 方式增强对象,然后返回这个对象。基本的寄生继承模式如下:

function createAnother(original){ 
let clone = object(original); // 通过调用函数创建一个新对象 
clone.sayHi = function() {    // 以某种方式增强这个对象 
console.log("hi"); 
}; 
return clone; // 返回这个对象
}

2.5、寄生式组合继承

组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是 创建子类原型时调用,另一次是在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所 有实例属性,子类构造函数只要在执行时重写自己的原型就行了。再来看一看这个组合继承的例子:

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.constructor = SubType;
SubType.prototype.sayAge = function() { 
console.log(this.age); 
};

终极版本:

function inheritPrototype(subType, superType) {
    // 创建对象prototype,相当于多了用于赋值的中间变量,省去了一次调用
    let prototype = object(superType.prototype); 
    prototype.constructor = subType; // 增强对象,解决由于重写原型导致默认constructor丢失的问题
    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);
    this.age = age;
} 
inheritPrototype(SubType, SuperType); 
SubType.prototype.sayAge = function() {
    console.log(this.age); 
};