1、前言
创建对象有很多种方法,最常见的是字面量创建和new Object()创建。但是在需要创建多个相同结构的对象时,这两种方法就不太方便了。
如:创建多个学生信息的对象
1 2 3 4 5 6 7 8 9 10 11 12 13
| let tom = { name: "Tom", age: 20, sex: "boy", height: 175 };
let marry = { name: "Marry", age: 22, sex: "girl", height: 165 }
|
2、对象工厂
2.1 实例
使用对象工厂改进上述代码,如:
1 2 3 4 5 6 7 8 9
| function person(name, age, sex, height) { return { name, age, sex, height } } let tom = new person("Tom", 20, "boy", 175) let marry = new person("Marry", 22, "girl", 165) console.log(tom); console.log(marry);
|
打印出结果:
{ name: ‘Tom’, age: 20, sex: ‘boy’, height: 175 }
{ name: ‘Marry’, age: 22, sex: ‘girl’, height: 165 }
对象工厂函数创建返回的是一个新对象。
2.2 缺陷以及解决方法
- 对象工厂本身是一个普通函数,用于表达对象结构时,描述性不强
- 对象工厂没有解决对象标识的问题,即创建的对象是什么类型。
- 利用构造函数可以解决这些问题
3、构造函数
3.1 实例详解
更改上面的代码,并添加一个say函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function person(name, age, sex, height) { this.name = name; this.age = age; this.sex = sex; this.height = height; this.say = function(){ console.log(`你好,我是${this.name}`); } } let tom = new person("Tom", 20, "boy", 175) let marry = new person("Marry", 22, "girl", 165) tom.say() marry.say()
|
打印出结果:
你好,我是Tom
你好,我是Marry
其中,代码中的this是对new Person的空对象进行扩展。
每个对象都具有constructor属性,用于标识对象的“类型”,如:
1 2
| console.log(Tom.constructor == Person); console.log(Tom.constructor == Object);
|
若要判tom和marry对象的类型,推荐使用instanceof方法。如:
1 2 3
| console.log(Tom instanceof Person); console.log(Tom instanceof Object);
|
但是这个解决方法还是有一些缺陷。
1 2
| console.log(Tom.say == Jerry.say);
|
输出是false的原因是因为同一个构造函数的实例都会创建一个自己的方法。这样可能会极大的增加的内存的负荷。而且,同一个方法应该是完成相同的任务,没有必要创建多个相同的方法。
我们可能会想到的解决方案是将它的say()方法提取出来,如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function Person(name, age, sex, height) { this.name = name; this.age = age; this.sex = sex; this.height = height; } function say() { console.log(`你好,我是${this.name}`); } let tom = new person("Tom", 20, "boy", 175) let marry = new person("Marry", 22, "girl", 165) tom.say() marry.say()
|
输出结果:
你好,我是Tom
你好,我是Marry
这样处理的好处就是say()方法不会被多次创建,但会产生一定的问题。即:
say() 为全局函数,会导致作用域混乱。而且只有Person创建的对象才能调用该方法,由于该方法是放在全局的,可能会产生内存泄漏,会让全局的函数更加臃肿。
解决方法:
利用原型模式,将方法定义在构造函数的原型对象上,可以解决这个问题
- 每个函数都有一个prototype属性,指向一个对象。
该对象包含应该由特定引用类型的实例共享的属性和方法。
该对象就是通过调用构造函数创建的对象的原型。
看下列代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function Person(name, age) { this.name = name this.age = age } Person.prototype.say = function () { console.log(`你好,我是${this.name}`); }
let Tom = new Person("Tom", 12) Tom.say() let Marry= new Person("Jerry", 10) Marry.say() console.log(Tom.say == Marry.say);
|
输出结果:
你好,我是Tom
你好,我是Marry
若要对person的原型进行属性扩展,可直接使用Person.prototype。因为当在构造函数原型上创建属性(或方法)时,会被改构造函数的额所有对象所共享。如:
1 2 3
| Person.prototype.from = "China" console.log(Tom.from); console.log(Marry.from);
|
若Marry对象本身有from属性,则继承自Person的from就不起作用了(原型的from属性被屏蔽掉了),起作用的是Marry自身的from属性,如:
1 2 3
| Marry.from = "America" console.log(Tom.from); console.log(Marry.from);
|
若要判断对象的原型,对象原型的构造函数,可枚举的属性:
1 2 3 4
| console.log(Object.keys(Marry)); console.log("from" in Marry); console.log(Object.getOwnPropertyNames(Marry)); console.log(Object.getPrototypeOf(Marry).constructor == Person);
|
3.2 基本原理总结
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
4、原型继承
4.1 概要
- 利用构造函数构建对象结构(模板,近似于类),从语义上较为清晰的表达对象结构。
- 利用构造函数原型扩展,能方便的为该构造函数所创建的对象进行基于原型的扩展。
- 利用构造函数还可以进行基于原型对象的继承
4.2 构造函数、原型和实例的关系
- 每个构造函数都有一个原型对象。
- 原型有一个属性指回构造函数
- 实例有一个内部指针[[Prototype]]指向原型。
4.3 原型链
当对象原型是另一个构造函数的实例,如此迭代,形成了一连串的继承关系,即为原型链。原型链表达了对象与对象之间的继承关系。
举个例子:
1 2 3 4 5 6 7 8 9 10
| function Person(name, age) { this.name = name this.age = age } let Marry = new Person("Marry", 10) console.log(Marry instanceof Person); console.log(Marry instanceof Object); console.log(Object.getPrototypeOf(Marry)); console.log(Object.getPrototypeOf(Object.getPrototypeOf(Marry)));
|
4.4 原型链的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Animal() { this.colors = ["white", "black"]; } function Mouse(name, age) { this.name = name; this.age = age; } Mouse.prototype = new Animal(); let m1 = new Mouse("Mickey", 10); console.log(m1.name, m1.colors); m1.colors.push("red"); let m2 = new Mouse("Miney", 9); console.log(m2.colors);
|
输出结果:
Mickey [ ‘white’, ‘black’ ]
[ ‘white’, ‘black’, ‘red’ ]
这是因为当原型中包含引用值,在实例件共享的是该引用值的引用,当修改实例中的该属性时,会影响全部实例。
存在的问题:子类型在实例化时不能给父类型传递参数。
解决方案:盗用构造函数
4.5 盗用构造函数
在子类构造函数中调用父类构造函数,并将子类当前实例只定为构造函数的上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function Animal(type) { this.colors = ["white", "black"]; this.type = type } function Mouse(name, age, type = "Mouse") { Animal.call(this, type) this.name = name; this.age = age; } Mouse.prototype = new Animal() let m1 = new Mouse("Mickey", 20) m1.colors.push("red") console.log(m1.name, m1.colors); let m2 = new Mouse("Miney", 18) console.log(m2.name, m2.colors);
console.log(m1 instanceof Mouse); console.log(m1 instanceof Animal);
|
输出结果:
Mickey [ ‘white’, ‘black’, ‘red’ ]
Miney [ ‘white’, ‘black’ ]
true
true
存在的问题:
无法访问父类原型上的方法,或者说是没有父类
解决方法:
将原型链与盗用构造函数结合起来
4.6 原型链与盗用构造函数的组合
将两者优点集中起来,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function Animal(type) { this.colors = ["white", "black"]; this.type = type } Animal.prototype.show = function () { console.log(this.type, this.colors); } function Mouse(name, age, type = "Mouse") { Animal.call(this, type) this.name = name; this.age = age; } Mouse.prototype = new Animal() let m1 = new Mouse("Mickey", 20) m1.colors.push("red") console.log(m1.name, m1.colors); m1.show()
let m2 = new Mouse("Miney", 18) console.log(m2.name, m2.colors); m2.show() console.log(Object.keys(m1));
|
存在的问题:
1 2
| console.log(m1 instanceof Mouse); console.log(m1 instanceof Animal);
|
构造函数的指向不正确,问题在于Mouse.prototype = new Animal(),将构造函数指向了Animal
解决方法:强制指定构造函数的指向为原型构造函数
1 2 3
| Mouse.prototype.constructor = Mouse console.log(m1.constructor == Mouse); console.log(m1 instanceof Mouse);
|
4、类
4.1 概述
- ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。
- 类( class)是ECMAScript 中新的基础性语法糖结构
- 虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念
4.2 实例一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function Person(name, age) { this.name = name; this.age = age }
Person.prototype.toString = function () { return `${this.name}:${this.age}` }
Person.prototype.setAttr = function (attr, value) { this[attr] = value } let people = [];
let p1 = new Person("tom", 20) let p2 = new Person("henry", 19) let p3 = new Person("mark", 21) let p4 = new Person("jeorge", 23) people.push(p1, p2, p3, p4)
people.sort((a, b) => a.age - b.age) console.log(people);
people.forEach((item) => console.log(item.toString()))
people[0].setAttr("gender", "Male") console.log(people[0]);
console.log(people[1]);
|
输出结果:
[
Person { name: ‘henry’, age: 19 },
Person { name: ‘tom’, age: 20 },
Person { name: ‘mark’, age: 21 },
Person { name: ‘jeorge’, age: 23 }
]
henry:19
tom:20
mark:21
jeorge:23
Person { name: ‘henry’, age: 19, gender: ‘Male’ }
Person { name: ‘tom’, age: 20 }
4.2 实例二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Person {
constructor(name, age) { this.name = name; this.age = age }; toString() { return `${this.name}:${this.age}` }; setAttr(attr, value) { this[attr] = value } } let people = [ new Person("tom", 20), new Person("henry", 19), new Person("mark", 21), new Person("jeorge", 23) ];
people.sort((a, b) => a.age - b.age) console.log(people);
people.forEach((item) => console.log(item.toString()))
people[0].setAttr("gender", "Male")
console.log(people[0]); console.log(people[1]);
|
输出结果:
[
Person { name: ‘henry’, age: 19 },
Person { name: ‘tom’, age: 20 },
Person { name: ‘mark’, age: 21 },
Person { name: ‘jeorge’, age: 23 }
]
henry:19
tom:20
mark:21
jeorge:23
Person { name: ‘henry’, age: 19, gender: ‘Male’ }
Person { name: ‘tom’, age: 20 }
4.3 实例三
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| class Person { constructor(name, age) { this.name = name; this.age = age }; toString() { return `${this.name}:${this.age}` } }
class Teacher extends Person { constructor(name, age, course) { super(name,age) this.course = course } }
let Liming = new Teacher("Liming",22,"语文") console.log(Liming);
class Student extends Person{ constructor(name, age, score) { super(name,age) this.score = score } }
let Tom = new Student("Tom",22,100) console.log(Tom);
console.log(Tom instanceof Student); console.log(Tom instanceof Person); console.log(Tom instanceof Object);
console.log(Object.getPrototypeOf(Liming)); console.log(Object.getPrototypeOf(Tom));
console.log(Object.getPrototypeOf(Student)); console.log(Object.getPrototypeOf(Teacher));
|
输出结果:
Teacher { name: ‘Liming’, age: 22, course: ‘语文’ }
Student { name: ‘Tom’, age: 22, score: 100 }
true
true
true
Person {}
Person {}
[class Person]
[class Person]
4.4 实例四
解法一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Person { constructor(name, age) { this.name = name; this.age = age } }
class Student extends Person { constructor(name, age, score) { super(name, age) this.score = score; }; get grade() { return this.__level || "NO GRADE" }; set grade(value) { this.__level = value.toUpperCase() } } let Tom = new Student("Tom", 22, 100) let Jerry = new Student("Jerry", 22, 100) Tom.grade = "grade one" console.log(Tom.grade); console.log(Jerry.grade);
|
输出结果:
GRADE ONE
NO GRADE
解法二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| let Person = function(){ return function(name,age){ this.name = name; this.age = age; } } class Students extends Person(){ #gd; constructor(name,age,score){ super(name,age) this.score = score } get grade(){ return this.#gd || "No GRADES" } set grade(value){ this.#gd = value.toUpperCase().trim() } } let p1 = new Person("Jerry",20) let p2 = new Person("Jerry") p1.grade = "abc" console.log(p1); console.log(p2);
|
输出:
[Function (anonymous)] { grade: ‘abc’ }
[Function (anonymous)]