对象 | 创建对象(含手写)

工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
function createPerson(name, age, job) {
let obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function() {
console.log(this.name); // 考考你:this指向哪里?
};
return obj;
};

let p1 = createPerson('Katrina', 18, 'student');
console.log(p1); // {name: 'Katrina', age: 18, job: 'student', sayName: ƒ}

优点

  • 可以解决创建多个类似对象的问题

缺点

  • 没有解决对象标识问题(即新创建的对象是什么类型)
    • 通俗来说,工厂模式中我们直接用new Object来创建,所以实例的原型链上只有Object.prototype对象

构造函数模式

1
2
3
4
5
6
7
8
9
10
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}

let p1 = new Person('Katrina', 18, 'student');

Person()构造函数代替了createPerson()工厂函数,实际上,Person()内部代码跟createPerson()基本一样,有如下区别:

  • 没有显示地创建对象
  • 属性和方法直接付给了this
  • 没有return

这就需要我们明白new操作符的机制了:手写原理 | new操作符

优点

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}

let p1 = new Person('Katrina', 18, 'student');
console.log(p1 instanceof Person); // true

缺点

  • 定义的每个方法会在每个实例上都创建一遍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}

let p1 = new Person('Katrina', 18, 'student');
let p2 = new Person('Kate', 20, 'doctor');

console.log(p1.sayName === p2.sayName) // false

/*
Person.prototype.sayHello = function() {
console.log(this.name);
}

console.log(p1.sayHello === p2.sayHello) // true
*/

在上面这个例子中,p1p2都有sayName方法,但这两个方法不是同一个sayName实例

ECMAScript中的函数是对象,因此每次定义函数时,都会初始化一个对象

1
2
3
4
5
6
7
8
9
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
/*
每个Person实例都会有自己的Function实例用于显示name属性
*/
this.sayName = new Function(console.log(this.name); ) // 逻辑等价
}

因为都是做同样一件事,所以定义两个Function实例是没必要的,要解决这个问题,可以把函数定义转移到函数外部

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}

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

let p1 = new Person('Katrina', 18, 'student');
let p2 = new Person('Kate', 20, 'doctor');

这里,sayName()被定义在了构造函数外部,在构造函数内部,sayName 属性等于全局sayName()函数

因为这一次sayName 属性中包含的只是一个指向外部函数的指针,所以person1 person2共享了定义在全局作用域上的sayName()函数

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法
补课:理解原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person() {}

Person.prototype.name = "Katrina";
Person.prototype.age = 18;
Person.prototype.job = "Student";
Person.prototype.sayName = function() {
console.log(this.name);
};

let person1 = new Person();
person1.sayName(); // "Katrina"

let person2 = new Person();
person2.sayName(); // "Katrina"

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

优点

  • 原型上定义的属性和方法可以被对象实例共享

缺点

  • 更改一个子类的引用,其他子类也会受到影响
1
2
3
4
5
6
7
8
9
10
function Person() {}
Person.prototype.colors = ['red', 'black', 'yellow'];

let p1 = new Person();
let p2 = new Person();

p1.colors[1] = 'pink';

console.log(p1.colors); // ['red', 'pink', 'yellow']
console.log(p2.colors); // ['red', 'pink', 'yellow']
  • 子类在实例化的时候不能向父类传参

构造函数模式+原型模式

属性写在构造函数模型中,方法写在原型模型中

这样的话,每个实例都有自己实例属性的副本,但同时又共享着对方法的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.colors = ['red', 'black', 'yellow'];
}

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

let p1 = new Person('Katrina', 18, 'student');
let p2 = new Person('Kate', 20, 'doctor');

console.log(p1.sayName == p2.sayName); // true

p1.colors[1] = 'pink';

console.log(p1.colors); // ['red', 'pink', 'yellow']
console.log(p2.colors); // ['red', 'black', 'yellow']