JavaScript关于创建对象和继承的一些总结

本文总结了JavaScript创建对象和实现继承的多种方式。

原文地址:https://www.zzfweb.cn/post/2017-12-26-create-obj.html

创建对象

普通方式

创建对象的普通方式有两种。
一种是:

  • 使用new操作符生成Object对象的实例
var obj = new Object();
obj.key = 'value';
console.log(obj);
// Object {key: "value"}
  • 使用对象字面量
var obj1 = {
    key1 : 'value'
};

console.log(obj1);
// Object {key1: "value"}

这两种方法在创建对象上效果是一样的,不过大多数时候,我们会使用第二种,这样比较简便。

工厂模式

function createObject(property){
    var o = {};
    o.property = property;
    o.doSomething = function(){
        console.log(this.property);
    }
    return o;
}

var obj1 = createObject('zzf');
obj1.doSomething();// zzf

此方法首先在内部实例化一个Object,然后设置它的属性和方法,然后再返回。
- 优点:能够无数次调用该函数,并返回指定的属性和方法,解决了创建多个相似对象的问题。
- 缺点:不能判断该对象的类型。

构造函数模式

function Planet(name, period){
    this.name = name;
    this.period = period;
    this.rotate = function(){
        console.log(this.name, '绕太阳旋转一周需要', this.period, '天');
    }
}
var earth = new Planet('earth', 365);
console.log(earth);
// Planet {name: "earth", period: 365, rotate: function}

此方法首先定义一个自定义类型的函数(构造函数),然后使用new操作符生成该类型的实例对象。
用上述方法创建对象会经历以下步骤:
1. 创建一个对象
2. 把构造函数里面的this指向新创建的对象(即this就是该心对象)
3. 执行构造函数里面的代码
4. 自动返回该新对象

注意:若直接调用构造函数,那么它的功能将与普通函数无异,构造函数里面的this将指向全局对象。

为防止直接调用函数,我们可以优化一下代码:

function Planet(name, period){
    if(!(this instanceof Planet)){
        return new Planet(name, period);
    }
    this.name = name;
    this.period = period;
    this.rotate = function(){
        console.log(this.name, '绕太阳旋转一周需要', this.period, '天');
    }
}

使用构造函数模式生成对象的优缺点:
- 优点:可以检测对象的类型。
- 缺点:每个函数都要在每个实例上重新创建一遍,导致不同的作用域链和标识符解析。由于有this对象的存在,创建两个完成同样任务的函数是没必要的。

原型模式

function Planet(){}
Planet.prototype.name = '地球';
Planet.prototype.period = '365';
Planet.prototype.rotate = function(){
        console.log(this.name, '绕太阳旋转一周需要', this.period, '天');
}

上述代码要多次使用prototype属性,有些累赘,我们可以优化为:

function Planet(){}
Planet.prototype = {
    constructor: Planet,
    name: '地球',
    period: '365',
    rotate: function(){
        console.log(this.name, '绕太阳旋转一周需要', this.period, '天');
    }
}

使用原型模式创建的实例对象,并不包含name、period和rotate这些属性和方法,但是其实例对象能够访问都这些属性和方法,因为这些属性和方法存在其构造函数的原型对象(prototype)上,实例对象也包含一个指针(__proto__)指向构造函数的原型对象上。当实例对象访问某个属性是,先在自身查找有没有该属性,有则返回,无则沿着原型链往上查找。

  • 优点:可以让所有对象实例共享它所包含的属性和方法,不必在构造函数中定义对象实例的信息。由于在原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都能够立即从实例上反映出来。
  • 缺点:共享所有属性,包括引用类型值的属性。若修改了引用类型值的属性,这将放映都其所有实例上。

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

function Planet(name, period){
    this.name = name;
    this.period = period;
}
Planet.prototype = {
    constructor: Planet,
    rotate: function(){
        console.log(this.name, '绕太阳旋转一周需要', this.period, '天');
    }
}

这种组合使用构造函数模式与原型模式生成对象的方式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。
- 优点:每个实例都会有自己的一份实例属性的副本,但同时有共享着对方的引用,最大限度地节省了内存。

动态原型模式

function Planet(name, period){
    this.name = name;
    this.period = period;
    if(typeof this.rotate != 'function'){
        Planet.prototype.rotate = function(){
                console.log(this.name, '绕太阳旋转一周需要', this.period, '天');
        }
    }
}

这种方法把所有信息都封装在构造函数中,通过检查某个应该存在的方法,来决定是否需要初始化原型。其中if语句检查其中一个即可。这种方法可以说是非常完美了。

继承

原型链继承

function Super(){
    this.key = 'Super Value';
}
Super.prototype.getValue = function(){
    return this.key;
}
function Sub(){
    this.subKey = 'Sub Value';
}
Sub.prototype = new Super();
Sub.prototype.getSubValue = function(){
    return this.subKey;
}
var sub = new Sub();
console.log(sub.getValue());
console.log(sub.getSubValue());
// Super Value
// Sub Value

使用原型链实现继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
上述代码有一行是将子类的原型赋值为父类的实例,这样子类便拥有父类的所有属性和方法。

子类的原型还包含一个指针,指向父类的原型:

Sub.prototype.__proto__ === Super.prototype;
// true

子类实例的constructor指向父类:

sub.constructor === Sub.prototype.constructor;
// true
Sub.prototype.constructor === Super.prototype.constructor
// true
Super.prototype.constructor === Super
// true

原型链继承存在的问题:
- 引用类型值的原型属性会被所有实例同享
- 不能向父类的构造函数传递参数

鉴于上述问题,实践中很少单独使用原型链继承。

借用构造函数继承

function Super(value){
    this.key = value;
}
function Sub(name){
    Super.call(this, 'value');
    this.name = name;
}
var sub = new Sub('sub');
console.log(sub);
// Sub {key: "value", name: "sub"}
  • 优点:子类构造函数可以向父类构造函数传递参数。
  • 缺点:方法都在构造函数中定义,无法复用,父类原型定义的方法对子类不可见。

由于上述缺点的存在,所以借用构造函数也是很少单独使用。

组合继承

function Super(value){
    this.key = value;
}
Super.prototype.getValue = function(){
    return this.key;
}
function Sub(name){
    // 借用构造函数继承,继承属性
    Super.call(this, 'value');
    this.name = name;
}
// 原型链继承,继承方法
Sub.prototype = new Super();
Sub.prototype.getSubName = function(){
    return this.name
}
var sub = new Sub('sub');
console.log(sub.getValue());
console.log(sub.getSubName());
// value
// sub

组合继承是JavaScript最常用的继承模式。
组合继承是把借用构造函数继承和原型链继承结合在一起,背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现实例属性的继承。

  • 缺点:无论什么情况都会调用两次父类构造函数,一次是创建子类型原型的时候,另一次是在子类型构造函内部。

原型式继承

function createObject(o){
    function F(){};
    F.prototype = o;
    return new F();
}
var anther = createObject(person);
anther.name = 'AA';
anther.friends.push('BB');
var other = createObject(person);
other.name = 'CC';
other.friends.push('DD');
console.log(anther);
console.log(other);
// F {name: "AA"}
// F {name: "CC"}
console.log(other.friends);
// ["B", "C", "BB", "DD"]

这种方法首先在内部创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。从本质上看,createObject对传入的参数做了一次浅复制,所以对于引用类型值的属性,会被createObject创建的对象共享。

我们也可以用Oject.create方法实现原型式继承(参考MDN):

// Shape - superclass
function Shape() {
  this.x = 0;
  this.y = 0;
}

// superclass method
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - subclass
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?',
  rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
  rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持相似的情况下,可以使用原型式继承。

寄生式继承

function createAnother(o){
    var clone = createObject(o);
    clone.say = function(){
        console.log('hello');
    }
    return clone;
}

在主要考虑对象而不是自定义类型和构造函数时,可以使用寄生式继承。对于createObject函数也不是必须的,任何能返回新对象的函数(比如Object.create)都使用这种模式。

寄生组合式继承

function inheritPrototype(subClass, superClass){
    var prototype = Object.create(superClass.prototype);
    prototype.constructor = subClass;
    subClass.prototype = prototype;
}
function Super(name){
    this.name = name;
}
Super.prototype.sayName = function(){
    console.log(this.name);
}
function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}

// 因为该函数会更改原型对象,所以要放在给子类原型添加方法的前面
inheritPrototype(Sub, Super);

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

var sub = new Sub('A', 18);
sub.sayName();
sub.sayAge();
// A
// 18

inheritPrototype函数共有三条语句,这三条语句的作用分别是创建对象、增强对象和指定对象。

寄生组合继承解决了组合继承多次调用父类构造函数的问题,避免了在父类原型上面创建不必要、多余的属性。

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


本文主要参考:

《JavaScript高级程序设计(第3版)》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值