02-继承与内置构造函数
# 内置构造函数
JavaScript 有很多内置构造函数,比如 Array
就是数组类型的构造函数,Function
就是函数类型的构造函数,Object
就是对象类型的构造函数。
内置构造函数非常有用,所有该类型的方法都是定义在它的内置构造函数的 prototype
上的,我们可以给这个对象添加新的方法,从而拓展某类型的功能。
// 数组的内置构造函数,任何的数组字面量都可以看做是Array的实例
console.log([1, 2] instanceof Array);
console.log([] instanceof Array);
var arr = new Array(5);
console.log(arr);
console.log(arr.length);
// 函数的内置构造函数,任何的函数字面量都可以看做是Function的实例
function fun() {
}
function add(a, b) {
return a + b;
}
console.log(fun instanceof Function);
console.log(add instanceof Function);
var jianfa = new Function('a', 'b', 'return a - b');
console.log(jianfa(8, 3));
// 对象的内置构造函数
console.log({a: 1} instanceof Object);
console.log({} instanceof Object);
var o = new Object();
o.a = 2;
o.b = 6;
console.log(o);
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
例:拓展数组的方法
console.log(Array.prototype.hasOwnProperty('push'));
console.log(Array.prototype.hasOwnProperty('pop'));
console.log(Array.prototype.hasOwnProperty('splice'));
// 拓展数组的求和方法
Array.prototype.qiuhe = function () {
// this表示调用qiuhe()方法的数组
var arr = this;
// 累加器
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
// 返回结果
return sum;
};
var arr = [3, 6, 2, 1, 3];
var result = arr.qiuhe();
console.log(result);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 内置构造函数的关系
Object.prototype
是万物原型链的终点。JavaScript 中函数、数组皆为对象,以数组为例,完整原型链是这样的:
console.log([1, 2].__proto__ === Array.prototype); // true
console.log([1, 2].__proto__.__proto__ === Object.prototype); // true
console.log([] instanceof Object); // true
console.log([] instanceof Array); // true
console.log(Object.prototype.__proto__); // null
console.log(null.__proto__); // 报错
2
3
4
5
6
7
8
内置构造函数的关系
任何函数都可以看做是 Function
“new出来的”,那我们开一个脑洞:Object
也是函数呀,它是不是Function
“new出来的”呢?答案是肯定的。
所以会有以下特殊的关系:
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.__proto__.__proto__ === Object.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Function); // true
console.log(Object instanceof Object); // true
2
3
4
5
6
7
8
# 继承
# 什么是继承
People 类和 Student 类的关系:
- People 类拥有的属性和方法 Student 类都有,Student 类还扩展了一些属性和方法;
- Student 是一种 People,两类之间是 “is a kind of” 关系;
- 这就是继承关系:Student 类继承自 People 类。
继承描述了两个类之间的 "is a kind of" 关系,比如学生“是一种”人,所以人类和学生类之间就构成继承关系。
People 是“父类”(或“超类”、“基类”);Student 是“子类”(或“派生类”),子类丰富了父类,让类描述得更具体、更细化。
# 通过原型链实现继承
实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能定义自己特有的属性和方法。
使用 JavaScript 特有的原型链特性来实现继承,是普遍的做法。在今后学习 ES6 时,将介绍新的实现继承的方法。
让子类构造函数的prototype,指向父类的一个实例
// 父类:People类
function People(name, age, sex) {
// this.arr = [33, 44, 55]; // 父类的属性中有引用类型值
this.name = name;
this.age = age;
this.sex = sex;
}
People.prototype.sayHello = function () {
console.log('你好,我是' + this.name + '我今年' + this.age + '岁了');
};
People.prototype.sleep = function () {
console.log(this.name + '正在睡觉');
};
// 子类:Student类
function Student(name, age, sex, school, sid) {
this.name = name;
this.age = age;
this.sex = sex;
this.school = school;
this.sid = sid;
}
// 实现继承的非常重要的语句。让子类的prototype指向父类的一个实例。
Student.prototype = new People();
// 追加方法
Student.prototype.exam = function () {
console.log(this.name + '正在考试');
};
Student.prototype.study = function () {
console.log(this.name + '正在学习');
};
// 子类可以更改父类的方法,术语叫做override“改写”、“重写”
Student.prototype.sayHello = function () {
console.log('敬礼!您好,我是' + this.name + ',我是' + this.school + '的学生,我' + this.age + '岁了');
};
var xiaoming = new Student('小明', 12, '男', '小慕学校', 100666);
xiaoming.exam();
xiaoming.study();
xiaoming.sayHello();
xiaoming.sleep();
// xiaoming.arr.push(123);
// console.log(xiaoming.arr);
var xiaohong = new Student('小红', 11, '女', '小慕学校', 100667);
// console.log(xiaohong.arr);
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
通过原型链实现继承的问题:
- 问题1:如果父类的属性中有引用类型值,则这个属性会被所有子类的实例共享;
- 问题2:子类的构造函数中,往往需要重复定义很多超类定义过的属性。即,子类的构造函数写的不够优雅。
# 借用构造函数
为了解决原型中包含引用类型值所带来问题和子类构造函数不优雅的问题,开发人员通常使用一种叫做**“借助构造函数”的技术,也被称为“伪造对象”或“经典继承”**。
借用构造函数的思想非常简单:在子类构造函数的内部调用超类的构造函数,但是要注意使用 call()
绑定上下文。
function People(name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
this.arr = [33, 44, 55];
}
function Student(name, sex, age, school, sid) {
People.call(this, name, sex, age);
this.school = school;
this.sid = sid;
}
var xiaoming = new Student('小明', '男', 12, '小慕学校', 100666);
console.log(xiaoming);
xiaoming.arr.push(77);
console.log(xiaoming.arr); // [33, 44, 55, 77]
console.log(xiaoming.hasOwnProperty('arr')); // true
var xiaohong = new Student('小红', '女', 11, '小慕学校', 100667);
console.log(xiaohong.arr); // [33, 44, 55]
console.log(xiaohong.hasOwnProperty('arr')); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
如果子类直接使用
People(name, sex, age)
会怎样呢?那这就是一次普通函数调用,三个属性会以window
对象为上下文,属性会设置到 window 对象上。所以需要使用 call 或者 apply 来指定函数上下文,将其指定为 Student 构造函数创建出来的对象。
借用构造函数解决了父类引用类型属性共享和子类构造函数重复定义属性的问题,但是只借助构造函数还并没有形成继承,即并没有把原型链连上。
# 组合继承
将借用原型链和借用构造函数的技术组合到一起,叫做组合继承,也叫作伪经典继承。
组合继承是 JavaScript 中最常用的继承模式。
// 父类
function People(name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
}
People.prototype.sayHello = function () {
console.log('你好,我是' + this.name + '今年' + this.age + '岁了');
}
People.prototype.sleep = function () {
console.log(this.name + '正在睡觉');
}
// 子类
function Student(name, sex, age, school, sid) {
// 借助构造函数
People.call(this, name, sex, age); //②
this.school = school;
this.sid = sid;
}
// 实现继承,借助原型链
Student.prototype = new People(); //①
// 子类拓展方法
Student.prototype.exam = function() {
console.log(this.name + '正在考试');
};
Student.prototype.sayHello = function() {
console.log('敬礼!你好,我是' + this.name + '今年' + this.age + '岁了,我是' + this.school + '学校的学生');
};
var xiaoming = new Student('小明', '男', 12, '小慕学校', 100666);
xiaoming.sayHello();
xiaoming.sleep();
xiaoming.exam();
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
组合继承实现了原型链的链接,也避免了借助原型链实现继承的缺点。
但是组合继承也有缺点。组合继承最大的问题就是无论什么情况下,都会调用两次超 类的构造函数:① 一次是在创建子类原型的时候;② 另一次是在子类构造函数的内部。
# 原型式继承
认识 Object.create()
IE9+ 开始支持 Object.create()
方法,可以根据指定的对象为原型创建出新对象。
// 以 obj1 原型,创建 obj2 对象
var obj2 = Object.create(obj1);
2
var obj1 = {
a: 33,
b: 45,
c: 12,
test: function() {
console.log(this.a + this.b);
}
};
// 第二个参数为需要补充的属性
var obj2 = Object.create(obj1, {
d: {
value: 99
},
a: {
value: 2
}
});
console.log(obj2.__proto__ === obj1); // true
console.log(obj2.a); // 2
console.log(obj2.b); // 45
console.log(obj2.c); // 12
console.log(obj2.d); // 99
obj2.test();
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
在没有必要“兴师动众”地创建构造函数,而只是想让新对象与现有对象"类似”的情况下,使用 Object.create()
即可胜任,称为原型式继承。
Object.create()
的兼容性写法:
// 道格拉斯·克罗克福德写的一个函数,非常巧妙,面试常考
// 函数的功能就是以o为原型,创建新对象
function object(o) {
// 创建一个临时构造函数
function F() {}
// 让这个临时构造函数的prototype指向o,这样一来它new出来的对象,__proto__指向了o
F.prototype = o;
// 返回F的实例
return new F();
}
var obj1 = {
a: 23,
b: 5
};
var obj2 = object(obj1);
console.log(obj2.__proto__ === obj1);
console.log(obj2.a);
console.log(obj2.b);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 寄生式继承
寄生式继承:编写一个函数,它接收一个参数 o,返回以 o 为原型的新对象 p,同时给 p 上添加预置的新方法
var o1 = {
name: '小明',
age: 12,
sex: '男'
};
var o2 = {
name: '小红',
age: 11,
sex: '女'
};
// 寄生式继承
function f(o) {
// 以o为原型创建出新对象
var p = Object.create(o);
// 补充方法
p.sayHello = function () {
console.log('你好,我是' + this.name + '今年' + this.age + '岁了');
}
p.sleep = function () {
console.log(this.name + '正在睡觉');
}
return p;
}
var p1 = f(o1);
p1.sayHello();
var p2 = f(o2);
p2.sayHello();
console.log(p1.sayHello == p2.sayHello); // false
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
寄生式继承就是编写一个函数,它可以"增强对象”,只要把对象传入这个函数,这个函数将以此对象为“基础”创建出新对象,并为新对象赋予新的预置方法。
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
寄生式继承的缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,即“方法没有写到 prototype
上”。
# 寄生组合式继承
之前提到的组合继承的缺点:组合继承最大的问题就是无论什么情况下,都会调用两次超类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数的内部。
寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
// 这个函数接收两个参数,subType是子类的构造函数,superType是父类的构造函数
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
subType.prototype = prototype;
}
// 父类
function People(name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
}
People.prototype.sayHello = function () {
console.log('你好,我是' + this.name + '今年' + this.age + '岁了');
}
People.prototype.sleep = function () {
console.log(this.name + '正在睡觉');
}
// 子类
function Student(name, sex, age, school, sid) {
// 借助构造函数
People.call(this, name, sex, age);
this.school = school;
this.sid = sid;
}
// Student.prototype = new People(); // 不需要再new一个People对象了
// 调用我们自己编写的inheritPrototype函数,这个函数可以让Student类的prototype指向“以People.prototype为原型的一个新对象”
inheritPrototype(Student, People);
Student.prototype.exam = function() {
console.log(this.name + '正在考试');
};
Student.prototype.sayHello = function() {
console.log('敬礼!你好,我是' + this.name + '今年' + this.age + '岁了,我是' + this.school + '学校的学生');
};
var xiaoming = new Student('小明', '男', 12, '小慕学校', 100666);
xiaoming.sleep();
xiaoming.exam();
xiaoming.sayHello();
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
# instanceof 运算符
instanceof
运算符用来检测"某对象是不是某个类的实例”。
if (xiaoming instanceof Student) {
console.log('xiaoming是Student对象');
}
2
3
底层机理:检查 Student.prototype
属性是否在 xiaoming
的原型链上(多少层都行,只要在就行)
function People() {
}
function Student() {
}
Student.prototype = new People();
var xiaoming = new Student();
// 测试 instanceof
console.log(xiaoming instanceof Student); // true
console.log(xiaoming instanceof People); // true
2
3
4
5
6
7
8
9
10