Skip to content

JavaScript深入之创建对象的多种方式以及优缺点 #15

@mqyqingfeng

Description

@mqyqingfeng
Owner

写在前面

这篇文章讲解创建对象的各种方式,以及优缺点。

但是注意:

这篇文章更像是笔记,因为《JavaScript高级程序设计》写得真是太好了!

1. 工厂模式

function createPerson(name) {
    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };

    return o;
}

var person1 = createPerson('kevin');

缺点:对象无法识别,因为所有的实例都指向一个原型

2. 构造函数模式

function Person(name) {
    this.name = name;
    this.getName = function () {
        console.log(this.name);
    };
}

var person1 = new Person('kevin');

优点:实例可以识别为一个特定的类型

缺点:每次创建实例时,每个方法都要被创建一次

2.1 构造函数模式优化

function Person(name) {
    this.name = name;
    this.getName = getName;
}

function getName() {
    console.log(this.name);
}

var person1 = new Person('kevin');

优点:解决了每个方法都要被重新创建的问题

缺点:这叫啥封装……

3. 原型模式

function Person(name) {

}

Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
    console.log(this.name);
};

var person1 = new Person();

优点:方法不会重新创建

缺点:1. 所有的属性和方法都共享 2. 不能初始化参数

3.1 原型模式优化

function Person(name) {

}

Person.prototype = {
    name: 'kevin',
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();

优点:封装性好了一点

缺点:重写了原型,丢失了constructor属性

3.2 原型模式优化

function Person(name) {

}

Person.prototype = {
    constructor: Person,
    name: 'kevin',
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();

优点:实例可以通过constructor属性找到所属构造函数

缺点:原型模式该有的缺点还是有

4. 组合模式

构造函数模式与原型模式双剑合璧。

function Person(name) {
    this.name = name;
}

Person.prototype = {
    constructor: Person,
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();

优点:该共享的共享,该私有的私有,使用最广泛的方式

缺点:有的人就是希望全部都写在一起,即更好的封装性

4.1 动态原型模式

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype.getName = function () {
            console.log(this.name);
        }
    }
}

var person1 = new Person();

注意:使用动态原型模式时,不能用对象字面量重写原型

解释下为什么:

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}

var person1 = new Person('kevin');
var person2 = new Person('daisy');

// 报错 并没有该方法
person1.getName();

// 注释掉上面的代码,这句是可以执行的。
person2.getName();

为了解释这个问题,假设开始执行var person1 = new Person('kevin')

如果对 new 和 apply 的底层执行过程不是很熟悉,可以阅读底部相关链接中的文章。

我们回顾下 new 的实现步骤:

  1. 首先新建一个对象
  2. 然后将对象的原型指向 Person.prototype
  3. 然后 Person.apply(obj)
  4. 返回这个对象

注意这个时候,回顾下 apply 的实现步骤,会执行 obj.Person 方法,这个时候就会执行 if 语句里的内容,注意构造函数的 prototype 属性指向了实例的原型,使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值,person1 依然是指向了以前的原型,而不是 Person.prototype。而之前的原型是没有 getName 方法的,所以就报错了!

如果你就是想用字面量方式写代码,可以尝试下这种:

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }

        return new Person(name);
    }
}

var person1 = new Person('kevin');
var person2 = new Person('daisy');

person1.getName(); // kevin
person2.getName();  // daisy

5.1 寄生构造函数模式

function Person(name) {

    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };

    return o;

}

var person1 = new Person('kevin');
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object)  // true

寄生构造函数模式,我个人认为应该这样读:

寄生-构造函数-模式,也就是说寄生在构造函数的一种方法。

也就是说打着构造函数的幌子挂羊头卖狗肉,你看创建的实例使用 instanceof 都无法指向构造函数!

这样方法可以在特殊情况下使用。比如我们想创建一个具有额外方法的特殊数组,但是又不想直接修改Array构造函数,我们可以这样写:

function SpecialArray() {
    var values = new Array();

    for (var i = 0, len = arguments.length; i < len; i++) {
        values.push(arguments[i]);
    }

    values.toPipedString = function () {
        return this.join("|");
    };
    return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
var colors2 = SpecialArray('red2', 'blue2', 'green2');


console.log(colors);
console.log(colors.toPipedString()); // red|blue|green

console.log(colors2);
console.log(colors2.toPipedString()); // red2|blue2|green2

你会发现,其实所谓的寄生构造函数模式就是比工厂模式在创建对象的时候,多使用了一个new,实际上两者的结果是一样的。

但是作者可能是希望能像使用普通 Array 一样使用 SpecialArray,虽然把 SpecialArray 当成函数也一样能用,但是这并不是作者的本意,也变得不优雅。

在可以使用其他模式的情况下,不要使用这种模式。

但是值得一提的是,上面例子中的循环:

for (var i = 0, len = arguments.length; i < len; i++) {
    values.push(arguments[i]);
}

可以替换成:

values.push.apply(values, arguments);

5.2 稳妥构造函数模式

function person(name){
    var o = new Object();
    o.sayName = function(){
        console.log(name);
    };
    return o;
}

var person1 = person('kevin');

person1.sayName(); // kevin

person1.name = "daisy";

person1.sayName(); // kevin

console.log(person1.name); // daisy

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。

与寄生构造函数模式有两点不同:

  1. 新创建的实例方法不引用 this
  2. 不使用 new 操作符调用构造函数

稳妥对象最适合在一些安全的环境中。

稳妥构造函数模式也跟工厂模式一样,无法识别对象所属类型。

下一篇文章

JavaScript深入之继承的多种方式和优缺点

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之new的模拟实现》

《JavaScript深入之call和apply的模拟实现》

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

Activity

xumengzi

xumengzi commented on Jun 8, 2017

@xumengzi

消灭零回复!

mqyqingfeng

mqyqingfeng commented on Jun 8, 2017

@mqyqingfeng
OwnerAuthor

@JarvenIV 哈哈,(๑•̀ㅂ•́)و✧

liuxinqiong

liuxinqiong commented on Jun 30, 2017

@liuxinqiong

感觉特么都有缺点啊

mqyqingfeng

mqyqingfeng commented on Jun 30, 2017

@mqyqingfeng
OwnerAuthor

@liuxinqiong 确实如此哈,一般都是用组合模式

ry928330

ry928330 commented on Aug 28, 2017

@ry928330

@mqyqingfeng 你好,想请问你两个问题:一个是:4.1动态原型的模式,“使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值”这是为什么?另一个是5.1 寄生构造函数模式,在调用的时候和工厂模式相比,就多了一个new调用,这个有什么用?

mqyqingfeng

mqyqingfeng commented on Aug 29, 2017

@mqyqingfeng
OwnerAuthor

@ry928330

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}

var person1 = new Person('kevin');

以这个例子为例,当执行 var person1 = new Person('kevin') 的时候,person1.的原型并不是指向 Person.prototype,而是指向 Person.prototype 指向的原型对象,我们假设这个原型对象名字为 O, 然后再修改 Person.prototype 的值为一个字面量,只是将一个新的值赋值给 Person.prototype, 并没有修改 O 对象,也不会切断已经建立的 person1 和 O 的原型关系,访问 person.getName 方法,依然会从 O 上查找

mqyqingfeng

mqyqingfeng commented on Aug 29, 2017

@mqyqingfeng
OwnerAuthor

@ry928330 关于寄生构造函数模式的使用,例子中也有讲到,你可以理解为这个人就是想用 new 的方式而不是直接调用函数 😂

ry928330

ry928330 commented on Aug 29, 2017

@ry928330

@mqyqingfeng 那现在怎么通过Person再找回原来的prototype(也就是O)呢,因为像这样
function Person(name) {
this.name = name;
}
var person1 = new Person('kevin');
person1.proto === Person.prototype //true
现在给Person.prototype重新赋值为一个字面量后,person1.proto === Person.prototype 肯定是false,那还能从Person这个构造函数(对象),找到原来的prototype么(也就是O)?

ry928330

ry928330 commented on Aug 29, 2017

@ry928330

@mqyqingfeng 那现在怎么通过Person再找回原来的prototype(也就是O)呢,因为像这样
function Person(name) {
this.name = name;
}
var person1 = new Person('kevin');
person1.proto === Person.prototype //true
现在给Person.prototype重新赋值为一个字面量后,person1.proto === Person.prototype 肯定是false,那还能从Person这个构造函数(对象),找到原来的prototype么(也就是O)?

54 remaining items

cw84973570

cw84973570 commented on Dec 10, 2020

@cw84973570
function Person(name) {
    this.name = name || 'keivn';
}

// 私有属性放到实例里
// Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
    console.log(this.name);
};

var person1 = new Person();

请问原型模式这样写有什么问题吗?跟其他的原型模式相比有什么优缺点?还是说这个name属性是作为公有属性的?

anjina

anjina commented on Dec 10, 2020

@anjina
function Person(name) {
    this.name = name || 'keivn';
}

// 私有属性放到实例里
// Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
    console.log(this.name);
};

var person1 = new Person();

请问原型模式这样写有什么问题吗?跟其他的原型模式相比有什么优缺点?还是说这个name属性是作为公有属性的?

你这是组合模式把, 构造函数 里面的 name 是每个实例对象的属性, 而原型上定义的才是实例对象共享的

cw84973570

cw84973570 commented on Dec 12, 2020

@cw84973570
function Person(name) {
    this.name = name || 'keivn';
}

// 私有属性放到实例里
// Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
    console.log(this.name);
};

var person1 = new Person();

请问原型模式这样写有什么问题吗?跟其他的原型模式相比有什么优缺点?还是说这个name属性是作为公有属性的?

你这是组合模式把, 构造函数 里面的 name 是每个实例对象的属性, 而原型上定义的才是实例对象共享的

@anjina 为什么文章里的组合模式要修改整个原型呢?在我的理解里最后实现的效果好像没区别,还是说两个写法都行?


又看了一遍,发现文章中说修改整个原型封装性会好点。。。。。

cw84973570

cw84973570 commented on Dec 15, 2020

@cw84973570

请问 第一个例子工厂模式,缺点是对象无法识别,我是这样理解,因为返回的是Object创建的对象,所以实例对象不能通过constructor找到对应的构造函数,但是你说的是因为所有的实例都指向同一个原型对象,能详细说下嘛

大概是如果var o = new Object()不做修改的话,不管是createPeople还是createPerson所创建的实例都是指向同一个原型,即对象o的原型永远指向Object.prototype.

haohongyang1

haohongyang1 commented on Dec 20, 2020

@haohongyang1

这个文章是在讲js继承吗

fatFire

fatFire commented on Mar 14, 2021

@fatFire
function person(name){
    var o = new Object();
    o.sayName = function(){
        console.log(name);
    };
    return o;
}

var person1 = person('kevin');

person1.sayName(); // kevin

person1.name = "daisy";

person1.sayName(); // kevin

console.log(person1.name); // daisy

稳妥稳妥构造函数模式 是不是sayName 这个函数在创建时作用域里保存了person函数的AO 所以每次调用都是kevin

ConanLF

ConanLF commented on Jun 16, 2021

@ConanLF

关于4.1的Person.prototype复写的理解:
//设地址是0x1234
var o = {
value: 1
}
//a.b指向0x1234
var a = {
b:o
}
var c = {}
//c.d指向a.b指向的地址 即0x1234
c.d = a.b
//设地址0x4321
var x = {
value: 2
}
//a.b指向新地址0x4321
a.b = x
//c.d地址是0x1234, a.b地址是0x4321
console.log(c.d === a.b) // false

把a.b换成Person.prototype, c.d换成person1.proto, o就是原来的原型实例, x就是复写的字面量
这样理解对不对

xiaqingping

xiaqingping commented on Aug 16, 2021

@xiaqingping

稳妥构造函数模式和工厂模式我咋就看着一样呢,区别在哪

wangxiaotian

wangxiaotian commented on Mar 7, 2022

@wangxiaotian

@mqyqingfeng
关于4.1看楼主解释和评论区好像明白了,但还有个困惑

`function Person(name) {
this.name = name;
}

Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
};
var person1 = new Person();`

4.0例子里边添加原型的时候也是重写的,为啥这个的实例原型和原构造函数原型没有断掉呢?
没看4.1的时候,看4.0是山;看了4.1,感觉4.0不是山了!

axiaoha

axiaoha commented on Apr 21, 2022

@axiaoha

@qujsh 原型也是一个对象,我们假设这个对象叫做 O,看这个例子:

var a = {
     b: O
}

你看 a.b 指向了 O 对象,就相当于 Person.prototype 指向了原型对象这句话。

再看这个例子:

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}

var person1 = new Person('kevin');

当 new Person() 的时候,是先建立的原型关系,即 person .proto = Person.prototype,而后修改了 Person.prototype 的值,这就相当于:

// O 表示原型对象
var O = {};

var a = {
     b: O
}

先建立原型关系,指的是 c.proto = a.b = O

而后修改 Person.prototype 的值,相当于

var anotherO = {};
a.b = anotherO;

即便修改了 Person.prototype 的值,但是 c.proto 还是指向以前的 O

不知道这样解释的清不清楚,欢迎交流~

image

function Person(name) {
   this.name = name
}

Person.prototype.getName = function () {
   console.log(this.name);
};
var person1 = new Person('kevin');
Person.prototype = {
   getName : 1
}
console.log(person1.__proto__);
console.log(person1.getName);
var person2 = new Person('kevin');
console.log(person2.__proto__);
console.log(person2.getName);
liynxy

liynxy commented on Jul 9, 2022

@liynxy

@mqyqingfeng 不知道对于这个 示例 4.1 的理解是否正确,还望能指出

function Person(name) {
    this.name = name;
    if (typeof this.getName != "function") {
        Person.prototype = {
            constructor: Person,
            getName: function () {
                console.log(this.name);
            }
        }
    }
}

var person1 = new Person('kevin');
var person2 = new Person('daisy');

// 报错 并没有该方法
person1.getName();

// 注释掉上面的代码,这句是可以执行的。
person2.getName();

个人对于原文这个示例的理解:

讨论中有个示例图,解释的比较清楚的一点是:js创建一个对象时是 先建立原型关系,而 后执行构造函数
那么在 第一个 var person1= new Person('Kevin') 调用的时候,函数(类)的 Person.prototype 还并没有被修改,然后再执行类似 Person.apply(obj) 的操作,在这个apply操作中,构造被执行,那么 if 里边的内容被执行,然后 Person.prototype 才被修改,指向新的一个字面量对象,
重点是,这个时候 person1 的原型还是指向的被 修改之前Person.prototype,而在第二次 var person2 = new Person('Daisy') 的时候,Person.prototype 已经被修改,因此 person1 原型上是没有 getName,而 person2 可以正常调用

我的理解是,实例是在原型重写之前还是之后创建的问题。
demo
创建 person1 实例的时候,关联的是最初的原型,然后原型就被重写了。

但在创建 person2 的时候,关联的就是重写之后的原型了。

而后面通过 return new Person(name) 这种方式来解决,其实就是在重写原型之后,再重新创建实例,这时候的实例关联的就是重写之后的原型。

Jamartin-create

Jamartin-create commented on Oct 12, 2022

@Jamartin-create

image
这个应该改成Array.push.apply(values, arguments); 吧

DaphnisLi

DaphnisLi commented on Nov 24, 2022

@DaphnisLi

怎么感觉在讲继承似的

tinyblckc0000al

tinyblckc0000al commented on Mar 21, 2024

@tinyblckc0000al

@mqyqingfeng 关于4.1看楼主解释和评论区好像明白了,但还有个困惑

`function Person(name) { this.name = name; }

Person.prototype = { constructor: Person, getName: function () { console.log(this.name); } }; var person1 = new Person();`

4.0例子里边添加原型的时候也是重写的,为啥这个的实例原型和原构造函数原型没有断掉呢? 没看4.1的时候,看4.0是山;看了4.1,感觉4.0不是山了!

4.0添加原型是在构造函数外添加的,只会执行一次,可以看成定义而不是重写,所有构造出来的对象实例引用的prototype都是同一个。
而4.1是每次构造函数都会执行,所以旧的对象所引用的prototype会因为新的对象构造过程而被覆盖。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @ry928330@nabei@geekzhanglei@mqyqingfeng@Xing-He

        Issue actions

          JavaScript深入之创建对象的多种方式以及优缺点 · Issue #15 · mqyqingfeng/Blog