Js ES5创建对象的九种方式

对象字面量

var obj = {
name: '顺鹏',
sex: 'man',
age: 21,
intro() {
  console.log(`姓名:${this.name}、性别:${this.sex}、年龄:${this.age}`);
}
}

使用new Object()进行定义(其实就是调用Object构造函数来构造对象)

JavaScript中,几乎所有的对象都是Object类型的实例,它们都会从Object.prototype继承属性和方法。

var obj = new Object();
obj.name = '顺鹏';
obj.sex = 'man';
obj.age = 21;
obj.eat = function() {
console.log(`${this.name}吃饭!`);
}

上面两种方法的缺点:代码复用性不高,当要创建多个对象时,会产生无法避免的代码冗余

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽离了创建对象的具体过程。考虑到在 ECMAScript 中无法创建类,开发人员发明一种函数,用函数来封装以特定接口创建对象的细节。

function createPerson(name, nickName, age, hobby) {
var per = new Object();
per.name = name;
per.nickName = nickName
per.age = age;
per.hobby = hobby;
per.sayHello = function () {
  console.log(`嘿!我名叫${this.name},外号${this.nickName},今年${this.age},爱好${this.hobby}`);
};
return per;
}
let zs = createPerson('张三', '法外狂徒', '33', '3年起步');
let lb = createPerson('老八', '美食家', '38', '吃粑粑');

优点

解决了创建多个相似对象时,代码的复用问题

缺点

  1. 创建不同对象其中属性和方法都会重复建立,消耗内存(每次创建属性和方法,都会为其在内存中单独开辟存储空间)

  2. 使用工厂模式创建的对象,没有解决对象识别的问题(就是怎样知道一个对象的类型是什么)

console.log(zs instanceof createPerson); // false
console.log(zs instanceof Object); // true

如上:对于外部而言,我明明是用createPerson创建的对象,结果他的类型却只是Object,没什么意义,因为所有对象都直接或间接是Object的实例。可能你创建这个工厂函数,你知道为啥,但是如果是别人使用你封装的这个方法,别人可不知道为啥, 别人就会产生上面的疑惑。

构造函数模式

构造函数可用来创建特定类型的对象。像 ObjectArray 这样的原生构造函数,在运行时会自动出现在执行环境中。此外我们也可以创建自定义的构造函数,从而定义对象类型的属性和方法。

// 自定义一个Person类
function Person(name, nickName, age, hobby) {
this.name = name;
this.nickName = nickName
this.age = age;
this.hobby = hobby;
this.sayHello = function () {
  console.log(`嘿!我名叫${this.name},外号${this.nickName},今年${this.age},爱好${this.hobby}`);
};
}
// 创建对象实例
let zs = new Person('张三', '法外狂徒', '33', '3年起步');
let lb = new Person('老八', '美食家', '38', '吃粑粑');

当我们使用构造函数实例化一个对象的时候,对象中会包含一个 __proto__ 属性指向构造函数原型对象,而原型对象中则包含一个constructor 属性指向构造函数。因此在实例对象中我们可以通过原型链来访问到constructor属性,从而判断对象的类型。

原型与原型链

优点

解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。例如Number构造函数与Array构造函数实例化出的对象分别属于Number类型、Array类型

缺点

我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayHello函数的实例,但它们其实做的都是同样的工作,因此这样便会造成内存的浪费。解决了工厂模式对象类型无法识别的问题,但还是未解决为相同方法重新建立内存存储、浪费内存的问题

原型模式

我们知道,我们创建的每一个函数都有一个 prototype 属性(引用数据类型),这个属性指向函数的原型对象

原型链内存示例图

这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。我们通过使用原型对象可以让所有的对象实例共享它所包含的属性和方法,因此这样也解决了代码的复用问题。如下面所示:

function Person() {

}

Person.prototype.name = '志盛';
Person.prototype.age = 22;
Person.prototype.hobby = 'study';
Person.prototype.sayHello = function () {
console.log(`我叫${this.name},今年${this.age},我没啥爱好,除了${this.hobby}`);
}

var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName === person2.sayName) // true

与构造函数模式不同的是,原型对象上的属性和方法是有所有实例所共享的。也就是说,上面 person1person2 访问的都是同一组属性和同一个 sayHello() 函数(同一块内存区域,不会再单独开辟内存)。

优点

解决了构造函数模式中多次创建相同函数对象造成的内存浪费的问题,所有的实例可以共享同一组属性和函数。

缺点

  1. 首先第一个问题是原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,会在一定程度上造成不方便。

  2. 因为所有的实例都是共享一组属性,对于包含基本值的属性来说没有问题,但是对于包含引用类型的值来说(例如数组对象),所有的实例都是对同一个引用类型进行操作,那么属性的操作就不是独立的,最后导致读写的混乱。我们创建的实例一般都是要有属于自己的全部属性的,因此单独使用原型模式的情况是很少存在的。

组合构造函数模式

创建自定义类型的最常见方式,就是组合使用构造函数模式原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。使用这种模式的好处就是,每个实例都会拥有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。而且这种混成模式还支持向构造函数传递参数,可以说是集两种模式之长与一身。

function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}

Person.prototype = {
constructor: Person,
sayHello: function () {
  console.log(`我叫${this.name},今年${this.age},我的爱好是${this.hobby}`);
}
}

var person1 = new Person('高顺鹏', 21, '玩游戏');
var person2 = new Person('张志盛', 22, '学习');
console.log(person1.sayHello === person2.sayHello); // true

优点

采用了构造函数模式和原型模式的优点,这种混成模式是目前使用最广泛,认同度最高的一种创建自定类型的方法。

缺点

由于使用了两种模式,因此对于代码的封装性来说不是很好。

动态原型模式

动态原型模式是解决上面混成模式存在封装性问题的一个方案。这个方法把所有信息都封装到了构造函数中,而在构造函数中通过判断只初始化一次原型。下面我们来看一下例子:

function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
console.log(typeof this.sayHello);
if (typeof this.sayHello !== 'function') {
  console.log('原型链上没有sayHello方法,所以我被执行了!目的是创建且仅创建一次sayHello方法,防止多次创建造成内存浪费');
  Person.prototype.sayHello = function() {
    console.log(`我叫${this.name},今年${this.age},我的爱好是${this.hobby}`);
  }
}
}

var person1 = new Person('奥特曼', 999, '打怪兽');
person1.sayHello(); 
// undefined
// 原型链上没有sayHello方法,所以我被执行了!目的是创建且仅创建一次sayHello方法,防止多次创建造成内存浪费
// 我叫奥特曼,今年999,我的爱好是打怪兽
var person2 = new Person('怪兽', 999, '打日本');
person2.sayHello();
// function
// 我叫怪兽,今年999,我的爱好是打日本
var person3 = new Person('李华', 22, '给国际友人写信');
person3.sayHello();
// function
// 我叫李华,今年22,我的爱好是给国际友人写信

注意在 if 语句中检查的可以是初始化后应该存在的任何属性或方法,不必要检查每一个方法和属性,只需要检查一个就行。

优点

解决了混成模式中封装性的问题

寄生构造函数模式

如果在前面几种模式不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。如下面的例子所示:

function Person(name, age, hobby) {
var per = new Object();
per.name = name;
per.age = age;
per.hobby = hobby;
per.sayHello = function () {
  console.log(`我叫${this.name},今年${this.age},我的爱好是${this.hobby}`);
};
return per;
}

var person1 = new Person('小明', 21, '打篮球');

从表面上看,这个函数很像是典型的工厂模式。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数(会污染其他的数组),因此可以使用这个模式:

function SpecialArray() {
 // 创建数组对象
 var values = new Array();
 // 添加值, arguments:每一个函数对象都有该属性,其代表该函数接收到的所有参数,是一个伪数组
 // values.push.apply(values, arguments);
 Array.prototype.push.apply(values, arguments);
 // 添加方法
 values.toPipedString = function () {
   return this.join('|');
 }
 // 返回数组
 return values
}
// 利用上面的SpecialArray构造函数,new一个具有toPipedString方法的特殊数组对象
var colors = new SpecialArray('red', 'green', 'blue')
console.log(colors.toPipedString());// red|green|blue

在这个例子中,创建了一个名叫SpecialArray的构造函数。在这个函数内部,首先创建了一个数组,然后push() 方法(用构造函数接收到的所有参数)初始化了数组的值。随后,又给数组实例添加了一个 toPipedString()方法,该方法返回以竖线分割的数组值。最后,将数组以函数值的形式返回。接着,调用了 SpecialArray函数,向其中传入了用于初始化数组的值,此后又调用了toPipedString()方法。

那么,以后我们只要想创建一个带有toPipedString()方法的特殊数组对象,就可以通过调用构造函数SpecialArray来生成此种特殊的数组实例

而在以前我们的做法可能是:

let arr = new Array();
console.log(arr);//就是一个空数组对象

arr.toPipedString = function() {
Array.prototype.push.apply(this, arguments);
return this.join('|');
}
/* arr.toPipedString = function(...params) {
    Array.prototype.push.call(this, params);
    return this.join('|');
  } */
console.log(arr);//拥有了toPipedString方法
console.log(arr.toPipedString(1, 2, 3));// 1|2|3

优点

我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。

缺点

和工厂模式一样的问题,不能依赖 instanceof 操作符来确定对象的类型。对于外部而言,我明明是用SpecialArray类创建的对象,结果他的类型却是Array,可能你创建这个类,你知道为啥,但是如果是别人使用你封装的这个类,别人可不知道为啥, 别人就会产生上面的疑惑。

关于寄生构造函数模式,需要说明的是:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象的类型。由于这个问题,推荐在能够使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

所谓稳妥对象,指的就是,没有公共属性,而且其方法也不使用 this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 thisnew),或者在防止数据被其他应用程序改动时使用。

稳妥构造函数遵循与寄生构造函数模式类似,但有两点不同:

  1. 新创建的对象的实例方法不引用 this

  2. 不使用 new操作符调用构造函数。

举个例子:

function Person(name, age, hobby) {
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function () {
  console.log(name);
}
//返回对象
return o;
}
var zs = Person('张三', 29, '唱歌');
zs.sayName();//'张三'

特点

  1. 函数会返回的对象,通过对象的方法我们可以访问或修改内部数据。

  2. 不可以通过为函数添加方法来访问、修改函数的内部数据。

意义

以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境提供的环境下使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鹏北海-RemHusband

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值