JS-对象

谈谈JS几种创建对象的方式~

创建对象

工厂模式

工厂模式,我们可以理解为doSometing、然后工厂将产品暴露出来的一个过程。这样可以解决创建多个相似对象的问题,但是区别不了对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createCar(brand, price, speed) {
var o = new Object();
o.brand = brand;
o.price = price;
o.speed = speed;
o.run = function () {
console.log(this.speed);
}
return o;
};

var audi = createCar('Audi', 100000, 100);
var ford = createCar('Ford', 10000, 120);

Object.prototype.toString.call(audi)
"[object Object]"

Object.prototype.toString.call(ford)
"[object Object]"

构造函数模式

构造函数有两种类型:

一种是原生的构造函数(例如Object和Array);

一种是自定义的构造函数:

1
2
3
4
5
6
7
8
9
10
function Car(brand, price, speed) {
this.brand = brand;
this.price = price;
this.speed = speed;
this.run = function () {
console.log(this.speed);
}
}
var audi = new Car('Audi', 100000, 100);
var ford = new Car('Ford', 10000, 120);

在构造函数中,按照惯例一般都会将函数名大写,非构造函数都小写。

构造函数本身也是函数,只是用来创建对象。要创建对象的实例,必须使用new 操作符。使用new一般会经历以下四个步骤:

  • 创建一个对象(继承Car. prototype)
  • 将构造函数的作用域赋给新对象(this指向这个新对象)
  • 执行构造函数的代码(为新对象添加属性)
  • 返回新对象(构造函数本身可以返回对象来进行覆盖,当然仅针对引用类型,基本数据类型不可以,基本数据类型不是对象)

上面的奥迪和福特分别保存着Car的一个实例,当然内存会为他们分配资源。这两个对象都有constructor的属性,该属性指向Car。使用构造函数我们可以将实例标识为特定的类型;使用instanceof 进行检测。

作为普通函数的构造函数

如果我们不使用new 操作符,属性和方法都会添加个全局对象,浏览器里面就是window对象:

1
2
Car('Ford', 10000, 120);
window.run(); // 120

构造函数的问题

先讲问题:每个方法都要在构造实例上重新创建一遍。奥迪和福特都有一个run的方法,但方法不是同一个Function的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Car(brand, price, speed) {
this.brand = brand;
this.price = price;
this.speed = speed;
this.run = run;
}

function run() {
console.log(this.speed);
}

var audi = new Car('Audi', 100000, 100);

audi.run();

缺点:

全局作用域下的方法又只能某个对象的实例所调用;
对象有很多方法,就需要定义多个全局的函数

我们可以通过原型模式来解决

原型模式

我们创建的每一个函数,都有一个prototype属性。这个属性是一个指针,指向一个对象,这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法。简单来说:prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

1
2
3
Car.prototype.run = function(first_argument) {
console.log(this.speed);
};

这样奥迪和福特的run方法都是访问的同一个run函数。

理解原型对象

无论什么时候,新建一个函数就会创建一个prototype的属性,这个属性是一个对象。

img

当我们调用audi.run时。会执行两次搜索,解析器会先查看实例audi有没有这个run属性。没有的话则继续向上查找知道原型。

当我们看到audi.constructor的属性指向了构造函数,其实是查找到的原型的constructor指向了构造函数。

原型与in操作符

有两种方式使用in。

一种是for-in,
一种是单独使用

单独使用时能够通过属性名判断定属性是否存在实例还是原型本身

可以通过hasOwnProperty来区别是存在实例本身,还是原型对象上。

通过Object.keys来获取可枚举属性的字符串数组

重写原型

1
2
3
4
5
6
7
8
9
10
function Car() {
}
Car.prototype = {
brand: 'Audi',
price: 100000,
speed: 100,
run: function() {
console.log(this.speed)
}
};

前面说过,创建一个函数,就会自动获取到原型对象,并且原型对象的constructor指向了构造函数。但是这里的原型对象被重写了,不再指向构造函数而是指向Object的构造函数,

instanceof 能够返回正确的结果,但是通过constructor已经无法确定类型。
1
2
3
4
5
const audi = new Car();
audi.constructor === Car // false
aud.constructor === Object // true
audi instanceof Object // true
audi instanceof Car // true

当然,如果constructor很重要,你可以给prototype指定一个constructor。这时的constructor变成可枚举属性。

原型的动态性

我们为原型添加属性时,所有的实例都可以访问到原型的属性

重写原型时。切断了构造函数与最初原型之间的联系

实例中的指针仅指向原型,而不是构造函数

原生对象的原型

所有原生引用类型都在其构造函数的原型上定义了方法。

1
Array.prototype.some

原型对象的问题

我们知道,所有的实例会共享原型;所以原型对象的属性进行修改时。所有实例的属性也会随之而变。但是实例一般都会有属于自己的属性。所有我们很少见到单独使用的原型。

构造函数 + 原型

构造函数: 用于定义实例属性

原型模式:定义方法以及共享的属性

两者结合:最大程度的节省了内存,每个实例又有自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
function Car() {
this.brand = brand;
this.price = price;
this.speed = speed;
}

Car.prototype = {
constructor: Car,
run: function() {
console.log(this.speed)
}
};

动态原型

1
2
3
4
5
6
7
8
9
10
function Car.() {
this.brand = brand;
this.price = price;
this.speed = speed;
if (typeof this.run !== 'function') {
Car.prototype.run =function (argument) {
console.log(this.speed);
}
}
}

使用动态原型时,不能使用重写原型的模式,否则会切断实例与原来原型的联系。

寄生构造

寄生构造我又将它称之为工厂构造。把工厂函数当成是构造函数使用。这里使用了new操作符,构造函数在没有任何返回的情况下,new 会帮我们返回一个新对象实例,寄生构造也返回了一个对象。这里返回的对象和new 出来的对象有什么不同呢?

1
2
3
4
5
6
7
8
9
10
function Car(brand, price, speed) {
var o = new Object();
o.brand = brand;
o.price = price;
o.speed = speed;
o.run = function () {
console.log(this.speed);
}
return o;
};
  • 寄生构造返回的对象与构造函数没有任何联系,我们也可以说新实例与构造函数、构造函数的原型没有任何联系。
  • 寄生构造在函数内外创建的实例对象没有区别,我们不能依赖instanceof 来确定对象的类型

稳妥构造

稳妥对象:没有公共属性,其方法也不引用this对象。稳妥构造与寄生构造类似,但有两点不同:

  • 新对象的实例方法不引用this
  • 不实用new操作符

对象字面量

以上,我们介绍了通过函数的方式介绍了对象。JS最简单也是使用得最多的其实是对象字面量

原型

1
2
3
var Jojo = {
name: 'Jojo'
};
1
Jojo.__proto__.constructor === Object // true

这种对象字面量默认继承了Object。在ES6以前,我们不得不使用Object.create()来设置原型。

这里有两种方式使用proto属性为对象字面量设置原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Man(color) {
this.color = color;
this.speak = function() {
console.log(this.color);
}
}
const AsiaMan = new Man('yellow');

way1:
var Jojo = {
__proto__: AsiaMan,
name: 'Jojo'
}
way2:
Jojo.__proto__ = AsiaMan;

方法速记

1
2
3
4
5
6
var Jojo = {
name: 'Jojo',
run: function() {
console.log(this.name)
}
}

方法可以简写为:

1
2
3
4
5
6
var Jojo = {
name: 'Jojo',
run () {
console.log(this.name)
}
}

super调用

super是用于原型链访问被继承的属性的一个快捷方法。

1
2
3
4
5
6
7
var Jojo = {
__proto__: AsiaMan,
name: 'Jojo',
run () {
super.speak() // 调用原型链上的方法
}
}

super的限制

  • 只能用在速记方法定义中