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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容