JavaScript创建对象之原型模式

一、原型基础


在之前的文章:《JavaScript创建对象之单例、工厂、构造函数模式》中详细介绍了构造函数模式创建对象的方式,构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的(实例识别)。

但是利用构造函数模式创建出来的每个对象,都拥有一份自己的属性和方法,拥有自己的属性是无可厚非的,但是方法应该是要共有的,不应该每个实例都有一份,每个对象都拥有一份方法的话,也会多占用内存空间。

于是基于构造函数的原型模式就有了,原型模式解决了方法或者属性不能共有的问题,在原型模式中,把实例之间相同的属性和方法提取成共有的属性和方法,即:想让谁共有,就把它放在类.prototype上。

function CreateJsPerson(name, age) {
    this.name = name; // p1.name=name
    this.age = age;
}

CreateJsPerson.prototype.writeJs = function () {
    console.log(this.name + ' write js');
};

var p1 = new CreateJsPerson('iceman' , 25);
var p2 = new CreateJsPerson('mengzhe' , 27);
    
console.log(p1.writeJs === p2.writeJs); // true

有三个非常重要的特性:

  • 每一个函数数据类型(普通函数、类)都有一个自带的属性:prototype(原型),并且这个属性是一个对象数据类型的值;

  • prototype上浏览器天生给它加了一个属性:constructor(构造函数),属性值是当前函数(类)本身;

  • 每一个对象数据类型(普通的对象、实例、prototype...)也天生自带一个属性:__proto__,属性值是当前实例所属的原型(prototype)。

看完以上三句话,是不是有些想吐了呢?哈哈,刚接触的时候都会感到一头雾水,接下来会慢慢讲解。但是别问为什么会有这三个结论,这都是浏览器自带的哦!

再看一个例子:

function Fn() {
    this.x = 100;
    this.sum = function () {}
}
Fn.prototype.getX = function () {
    console.log(this.x);
};
Fn.prototype.sum = function () {
};
var f1 = new Fn();
var f2 = new Fn();

console.log(Fn.prototype.constructor === Fn); // true

下图为该例子对应的图解(为了不增加难度,只画堆内存),注意联系上面的三个结论来理解哦:

原型基础.png

Object是JavaScript中所有数据类型的基类(最顶层的类):

  • f1 instanceof Object 输出true,因为f1通__proto__,可以向上级查找,不管多少级,最后总能找到Object;
  • 因为是最顶层的类了,所以Object.prototype上没有__proto__这个属性;

二、原型链模式


f1.hasOwnProperty('x'),f1能调用hasOwnProperty,那么hasOwnProperty是f1的一个属性。但是我们发现f1的私有属性上并没有这个方法,那如何处理的呢:

  • 通过 对象名.属性名 的方式获取属性值的时候,首先在对象的私有属性上进行查找,如果私有的属性中存在这个属性,则获取的是私有的属性值;

  • 如果私有的属性中没有,则通过__proto__找到所属类的原型(类的原型上定义的属性和方法都是当前实例的公有的属性和方法),原型上存在的话,获取的是原型上公有的属性值;

  • 如果原型上也没有,则继续通过原型上的__proto__继续向上查找,一直找到Object.prototype为止。

以上的这种查找机制,就是原型链模式

console.log(f1.getX == f2.getX); // true
console.log(f1.__proto__.getX == f2.__proto__.getX); // true
console.log(f1.getX === Fn.prototype.getX); // true

console.log(f1.sum === f2.prototype.sum); // false
console.log(f1.sum === Fn.prototype.sum); // false

注意在IE浏览器中,原型模式也是这个原理,但是IE浏览器怕你通过__proto__把公有的修改,禁止我们使用__proto__

三、原型模式中的this


在原型模式中,this常见的情况有两种:

  • 在类中:this.xxx = xxx; this表示当前类的实例;

  • 在某一个方法中:要看"."前面是谁this就是谁,通过以下的三个步骤:

    • 需要先确定this的指向(this是谁);
    • 把this替换成对应的代码;
    • 按照原型链查找的机制,一步步的查找结果;
function Fn() {
    this.x = 100;
    this.y = 200;
    this.getY = function () {
        console.log(this.y);
    }
}
Fn.prototype = {
    constructor:Fn,
    y:300,
    getX : function () {
        console.log(this.x);
    },
    getY : function () {
        console.log(this.y);
    }
}
var f = new Fn;
f.getX(); // --> console.log(f.x) --> 100
f.__proto__.getX(); // --> this是f.__proto__ --> console.log(f.__proto__.x) --> undefined

Fn.prototype.getX(); // --> undefined

f.getY(); // --> 200

f.__proto__.getY(); // --> 300

四、在内置类的原型上扩展方法


在Array类的原型上扩展一个去重的方法:

Array.prototype.myUnique = function () {
    var obj = {};
    for (var i = 0; i < this.length; i++) {
        var cur = this[i];
        if(obj[cur] == cur) {
            this[i] = this[this.length - 1];
            this.length --;
            i--;
            continue;
        }
        obj[cur] = cur;
    }
    obj = null;
    return this; // 返回this目的是为了实现链式写法
};
var ary = [12, 23, 23, 13, 12, 13, 23, 13];
ary.myUnique();
console.log(ary);
ary.myUnique().sort(function (a, b) {
    return a - b;
});
console.log(ary);

Array.prototype.myUnique(); // this --> Array.prototype

链式写法:执行完数组的一个方法可以紧接着执行下一个方法(jQuery中实现了链式写法)。

ary.sort(function (a, b) {
    return a - b;
}).reverse().pop();
console.log(ary);
  • ary为什么可以使用sort方法呢?因为sort是Array.prototype上的公有方法,而数组ary是Array这个类的一个实例,所以ary可以使用sort方法,也就是数组才能使用Array原型上定义的属性和方法;

  • sort执行完成的返回值是一个排序后数组,可以继续执行reverse;

  • reverse执行完成的返回值是一个数组,可以继续执行pop;

  • pop执行完成的返回值是被删除的那个元素,不是一个数组了,所以再执行push会报错。

五、批量设置原型上的公有属性和方法


5.1、为原有函数的prototype起一个别名

function Fn() {
    this.x = 100;
}
var pro = Fn.prototype; // 把原来原型指向的地址赋值给我们的pro,现在它们操作的是同一个内存空间
pro.getX = function () {
};
pro.getY = function () {
};
var f1 = new Fn();

jQuery中就是这么实现的。

5.2、重构原型对象的方式

自己新开辟一个新内存,存储我们公有的属性和方法,把浏览器原来给Fn.rototype开辟的那个替换掉:

function Fn() {
    this.x = 100;
}
Fn.prototype = {
    constructor:Fn,
    a:function () {
        
    },
    b:function () {
        
    }
};
var f = new Fn;
批量修改原型的方法.png

只有浏览器天生给Fn.prototype开辟的堆内存里面才有constructor,而我们自己开辟的这个堆内存没有这个属性,这样constructor指向的就不是Fn而是Object

console.log(f.constructor); // --> 没做处理之前输出 Object

为了和原来的保持一致,我们需要手动的增加constructor的指向:

constructor:Fn

注意:不能将这种方式用于给内置类增加公有的属性,例如:

Array.prototype = {
    constructor:Fn,
    myUnique:function () {
    }
};
console.dir(Array.prototype);

因为如果这种方式能用于内置类的话,会将之前内置类中已经存在于原型上的属性和方法给替换掉,所以浏览器是屏蔽这种方式修改内置类的

所以如果想给内置类增加公有方法的话,应该使用如下方式:

Array.prototype.myUnique = function () {

};

但是这种方式也是有危险的,因为我们可以一个一个的修改内置类的方法,当通过以下的方式在数组的原型上增加方法,如果方法名和原来内置的方法名重复,会把内置类内置的公有方法修改掉,所以以后在内置类的原型上增加方法的时候,命名都需要加特殊的前缀。

Array.prototype.sort = function () {
    // .....
};

六、继承


6.1、原型继承

function A() {
    this.x = 100;
}
A.prototype.getX = function () {
    console.log(this.x);
};
function B() {
    this.x = 200;
};
B.prototype = new A();

var n = new B;

原型继承是JavaScript中最常用的一种方式。

子类B想要继承父类A中的所有的属性和方法(私有+公有),只需要让B.prototype = new A; 即可。

原型继承的特点:它是把父类中私有的+公有的都继承到了子类的原型上(子类公有的)。

核心:原型继承,并不是把父类中的属性和方法克隆一份一模一样的给B,而是让B和A之间增加了原型链的连接,以后B的实例n想要用A中的getX方法,需要一级级的向上查找来使用。

6.2、call继承

function A() {
    this.x = 100;
}
A.prototype.getX = function () {
    console.log(this.x);
}

function B() {
    // this --> n
    A.call(this); // --> A.call(n) 把A执行,让A中的this变为了n
}

var n = new B;
console.log(n.x);;

call继承是把父类私有的属性和方法,克隆一份一模一样的作为自己的私有的属性,注意:只有私有的属性和方法才能继承。公有的属性和方法是没法继承的。所以如果执行n.getX()会报错:

Uncaught TypeError: n.getX is not a function

6.3、冒充对象继承

function A() {
    this.x = 100;
}
A.prototype.getX = function () {
    console.log(this.x);
}

function B() {
    // this --> n
    var temp = new A;
    for (var key in temp) {
        this[key] = temp[key];
    }
    temp = null;
}
    
var n = new B;
console.log(n.x);;

冒充对象继承会把父类 私有的+公有的 都克隆一份一模一样的给子类私有的。

6.4、混合模式继承

function A() {
    this.x = 100;
}
A.prototype.getX = function () {
    console.log(this.x);
}

function B() {
    A.call(this); // --> n.x = 100;
}
B.prototype = new A; // --> B.prototype: x=100  getX
B.prototype.constructor = B;
    
var n = new B;
n.getX();

混合模式继承就是:原型继承+call继承。

使用混合模式继承可以让子类即拥有父类私有的属性和方法(call继承的特点),又拥有父类公有的属性和方法(原型继承的特点),但是有一个问题是,父类中私有属性也会成为子类的公有属性,比如本例中的B类中,在私有属性和原型上都拥有一个x=100,虽然根据原型链搜索原则,在使用的没有影响,但是作为有一个代码洁癖的程序员还是觉得不妥,因为毕竟是占用了那么一丢丢的空间(哈哈),那么这时候就可以看接下来的寄生组合式继承了。

6.5、寄生组合式继承

function A() {
    this.x = 100;
}

A.prototype.getX = function () {
    console.log(this.x);
}

function B() {
    A.call(this);
}
//B.prototype = Object.create(A.prototype); // IE6、7、8不兼容
B.prototype = objectCreate(A.prototype);
B.prototype.constructor = B;
    
var n = new B;
console.dir(n);

function objectCreate(o) {
    function fn() {}
    fn.prototype = o;
    return new fn;
}

6.6、中间类继承

首先声明,这种中间类继承是不兼容的,但是可以用于移动端,因为移动端不用兼容IE,并且这种方式在大多数书中都没有介绍,算是一种奇技淫巧吧。

function avgFn() {
    arguments.__proto__ = Array.prototype;
    arguments.sort(function (a, b) {
        return a-b;
    })
    arguments.pop();
    arguments.shift();
    return eval(arguments.join('+')) / arguments.length;
}
console.log(avgFn(10, 20, 30, 10, 30, 30, 40));

个人公众号(icemanFE):分享更多的前端技术和生活感悟

个人公众号.png

推荐阅读更多精彩内容