JavaScript 创建对象 3 原型模式

我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例信息,而是可以将这些信息直接添加到原型对象中。

        function Person() {
        }

        Person.prototype.name = "Neo";
        Person.prototype.age = 29;
        Person.prototype.job = "Teacher";
        Person.prototype.sayName = function() {
            console.log(this.name);
        }

        var person1 = new Person();
        person1.sayName();
        var person2 = new Person();
        person2.name = "Toby";
        person2.age = 30;
        person2.sayName();
        console.log(person1.sayName === person2.sayName);

上面的代码的输出是:

代码的输出
  1. 理解原型对象

原型模式,有点类似于 C++ 的继承,每个对象都继承了来自原型对象中的值,这种继承是只读的,我们不能重写原型对象中的值,如果我们在实例中对原型对象中的同名属性赋值,其实是在实例中添加了一个新的属性,这个属性会屏蔽来自原型对象中的属性,而这点就类似于 C++ 中的覆盖。下面是实例:

        function Person() {
        }

        Person.prototype.name = "Neo";
        Person.prototype.age = 29;
        Person.prototype.job = "Teacher";
        Person.prototype.sayName = function() {
            console.log(this.name);
        }

        var person1 = new Person();
        person1.sayName(); // 来自原型的值
        var person2 = new Person();
        person2.name = "Toby";
        person2.age = 30;
        person2.sayName(); // 来自实例的值
        person1.sayName(); // 来自原型的值

输出结果:

输出结果

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

我们已经知道,如果我们在实例中对原型对象中的同名属性赋值,其实是在实例中添加了一个新的属性,这个属性会屏蔽来自原型对象中的属性。即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete 操作符则可以完全删除实例属性,从而让我们可以重新访问原型中的属性,如下所示:

        function Person() {
        }

        Person.prototype.name = "Neo";
        Person.prototype.age = 29;
        Person.prototype.job = "Teacher";
        Person.prototype.sayName = function() {
            console.log(this.name);
        }

        var person2 = new Person();
        person2.name = "Toby";
        person2.age = 30;
        person2.sayName(); // 来自实例的值
        delete person2.name;
        person2.sayName(); // 来自原型的值

输出结果:

输出结果

使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘记它是从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。下面是个实例:

        function Person() {
        }

        Person.prototype.name = "Neo";
        Person.prototype.age = 29;
        Person.prototype.job = "Teacher";
        Person.prototype.sayName = function() {
            console.log(this.name);
        }

        var person1 = new Person();
        person1.sayName(); // 来自原型的值
        console.log("person1.hasOwnProperty(\"name\") :", person1.hasOwnProperty("name"));

        var person2 = new Person();
        person2.name = "Toby";
        person2.age = 30;
        console.log("person2.hasOwnProperty(\"name\") :", person2.hasOwnProperty("name"));
        person2.sayName(); // 来自实例的值

        delete person2.name;
        person2.sayName(); // 来自原型的值
        console.log("person2.hasOwnProperty(\"name\") :", person2.hasOwnProperty("name"));

实例的输出结果:

输出结果
  1. 原型与 in 操作符

有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中,下面是使用实例:

        function Person() {
        }

        Person.prototype.name = "Neo";
        Person.prototype.age = 29;
        Person.prototype.job = "Teacher";
        Person.prototype.sayName = function() {
            console.log(this.name);
        }

        var person1 = new Person();
        person1.sayName(); // 来自原型的值
        console.log("person1.hasOwnProperty(\"name\"): ", person1.hasOwnProperty("name"));
        console.log("\"name\" in person1: ", "name" in person1);

        var person2 = new Person();
        person2.name = "Toby";
        person2.age = 30;
        console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
        console.log("\"name\" in person2: ", "name" in person2);
        person2.sayName(); // 来自实例的值

        delete person2.name;
        person2.sayName(); // 来自原型的值
        console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
        console.log("\"name\" in person2: ", "name" in person2);
        console.log("\"gender\" in person2: ", "gender" in person2);

输出结果:

输出结果

同时使用 hasOwnProperty 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。

        function hasPrototypeProperty(object, propertyString) {
            return !object.hasOwnProperty(propertyString) && (propertyString in object);
        }

下面是其使用实例:

        function hasPrototypeProperty(object, propertyString) {
            return !object.hasOwnProperty(propertyString) && (propertyString in object);
        }

        function Person() {
        }

        Person.prototype.name = "Neo";
        Person.prototype.age = 29;
        Person.prototype.job = "Teacher";
        Person.prototype.sayName = function() {
            console.log(this.name);
        }

        var person1 = new Person();
        person1.sayName(); // 来自原型的值
        console.log("person1.hasOwnProperty(\"name\"): ", person1.hasOwnProperty("name"));
        console.log("\"name\" in person1: ", "name" in person1);
        console.log("hasPrototypeProperty(person1, \"name\"): ", hasPrototypeProperty(person1, "name"));

        var person2 = new Person();
        person2.name = "Toby";
        person2.age = 30;
        console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
        console.log("\"name\" in person2: ", "name" in person2);
        console.log("hasPrototypeProperty(person2, \"name\"): ", hasPrototypeProperty(person2, "name"));
        person2.sayName(); // 来自实例的值

        delete person2.name;
        person2.sayName(); // 来自原型的值
        console.log("person2.hasOwnProperty(\"name\"): ", person2.hasOwnProperty("name"));
        console.log("\"name\" in person2: ", "name" in person2);
        console.log("hasPrototypeProperty(person2, \"name\"): ", hasPrototypeProperty(person2, "name"));

其输出结果如下:

输出结果
  1. 更简单的原型语法
        function Person() {
        }

        Person.prototype = {
            name: "Neo",
            age: 29,
            job: "Teacher",
            sayName: function() {
                console.log(this.name);
            }
        }
        // 以下代码,确保通过 constructor 属性还能像之前的语法那样能够访问到适当的值
        Object.defineProperty(Person.prototype, "constructor", {
                                  enumerable: false,
                                  value: Person
                              });
  1. 原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来 —— 即使是先创建了实例后修改原型也照样如此,下面是一个实例:

        function Person() {
        }

        var friend = new Person();

        Person.prototype.sayHi = function() {
            console.log("hi");
        }

        friend.sayHi();

输出结果:

输出结果

但是如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型 [[prototype]] 的指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。下面是一个实例:

        function Person() {
        }

        var friend = new Person();

        Person.prototype = {
            name: "Neo",
            age: 29,
            job: "Teacher",
            sayName: function() {
                console.log(this.name);
            }
        }
        // 以下代码,确保通过 constructor 属性还能像之前的语法那样能够访问到适当的值
        Object.defineProperty(Person.prototype, "constructor", {
                                  enumerable: false,
                                  value: Person
                              });

        friend.sayName();

这个例子会报错:

例子报错

因为 friend 指向的原型中不包含以 sayName 命名的函数。重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

  1. 原型对象的问题

原型模式也不是没有缺点。首先,他省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。

原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。请看下面这个例子:

        function Person() {
        }

        Person.prototype = {
            name: "Neo",
            age: 29,
            job: "Teacher",
            friends: ["Toby", "Tina"],
            sayName: function() {
                console.log(this.name);
            }
        }
        // 以下代码,确保通过 constructor 属性还能像之前的语法那样能够访问到适当的值
        Object.defineProperty(Person.prototype, "constructor", {
                                  enumerable: false,
                                  value: Person
                              });

        var person1 = new Person();
        var person2 = new Person();

        person2.friends.push("Tim");

        console.log(person1.friends);
        console.log(person2.friends);
        console.log(person1.friends === person2.friends);

输出结果:

输出结果

在此,假如我们的初衷就是所有对象共享一个 friends 数组的话,是没有问题的。但是现实中,这样的情况少之又少,因此,我们不应该单独使用原型模式,我们该怎么做呢?请关注下一节中介绍的内容。

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

推荐阅读更多精彩内容