ES5中实现继承
# 对象原型
JavaScript中每个对象都会有一个特殊的内置属性[[prototype]]
,这个特殊的对象可以指向另外一个对象。
这个对象的作用
- 当通过引用对象那个的属性key来获取一个value时,会触发
[[get]]
的操作 - 这个操作会首先检查该对象是否有对应的属性,如果有就使用它
- 如果对象中没有这个属性,那么访问对象
[[prototype]]
内置的这个属性
如果通过字面量直接创建一个对象,那么这个对象也会有一个这样的属性,只要是对象都会有一个这样的属性
获取原型对象上属性的方法
- 通过对象的
__proto__
属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题) - 通过
Object.getPrototypeOf
方法获取
var obj = {
name: 'ls',
age: 18
}
console.log(obj)
console.log(obj.name, obj.age)
// 获取对象的原型(非标准)
console.log(obj.__proto__)
// 标准获取对象原型
console.log(Object.getPrototypeOf(obj))
console.log(obj.__proto__ === Object.getPrototypeOf(obj)) // true
/*
obj.name 实际上会触发[[get]]的操作获取obj对象上name属性的值
1. 优先在本身对象(obj)上面查找,如果找到直接返回value】
2. 如果没有找到就会去原型对象上查找
*/
// console.log(obj.message) // undefined
obj.__proto__.message = 'Hello World'
console.log(obj.message) // Hello World
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 函数的原型 prototype
只有函数才有prototype
的属性,对象没有这个属性
var obj = {}
function foo(){}
// 将函数看做是一个普通的对象时,是具备__proto__(隐式原型)
// 作用:查找key对应的value时,会找到原型身上
console.log(obj.__proto__)
console.log(foo.__proto__)
// 将函数看成是一个函数时,它是具备prototype(显式原型)
// 作用:用来构建对象时,给对象设置隐式原型
console.log(foo.prototype)
console.log(obj.prototype) // 对象是没有prototype的
2
3
4
5
6
7
8
9
10
# new 操作符
使用new关键字创建对象时,对象内部的[[prototype]]
属性会被赋值为该构造函数的prototype
属性(将函数的显示原型赋值给这个对象作为它的隐式原型).
也就是说通过Person构造函数创建出来的所有对象的[[prototype]]
属性都指向Person.prototype
function Person(){
}
var p1 = new Person()
// 1. 内存中创建一个新的对象
p = {}
// 2.this指向这个空对象
this = p
// 3. 函数的显示原型赋值给对象作为对象的隐式原型
p.__proto__ = Person.prototype
var p2 = new Person()
console.log(p1.__proto__===p2.__proto__) // true
2
3
4
5
6
7
8
9
10
11
12
13
# 将方法放在原型上
function Person(name, age, sno) {
this.name = name;
this.age = age;
this.sno = sno;
// 方式一:在函数内部编写对应的方法,这种方式在每次创建新的对象时都会创建新的方法函数
/* this.running = function () {
console.log(this.name + " running");
}; */
}
// 方式二:将方法放在原型上
// 当多个对象拥有共同的值时,可以将它放到构造函数对象的显示原型上
// 由构造函数创建出来的所有对象会共享这些属性
// 即当创建多个对象时,对象中的方法指向的都是同一个
Person.prototype.running = function(){
console.log(this.name + " running"); // 这里的this是一个隐式绑定
}
var p1 = new Person("zs", 18, 111);
var p1 = new Person("ls", 20, 112);
// 先在自己身上查找running函数,没有找到就会去原型上面查找
// 函数原型的作用:
// 在通过new操作创建对象时,将这个显式原型赋值给创建出来对象的隐式原型
/*
为什么属性不能放在原型上
1. 因为每个对象上的属性值是不相同的,如果将属性放在原型上,意味着属性值只会保存一份,每次创建新的对象,新创建的对象的属性值会覆盖到前面创建对象的属性值
*/
p1.running();
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
# constructor
事实上原型对象上面都是有一个非常重要的属性:constructor
默认情况下原型上都会添加一个属性叫做constructor
,这个constructor
指向当前的函数对象
function Person(){}
var pPrototype = Person.prototype
console.log(pPrototype) // {constructor}
console.log(pPrototype.constructor) // Person(){}
console.log(pPrototype.constructor === Person) // true
console.log(Person.name) // Person
console.log(pPrototype.constructor.name) // Person
/*
函数中非常重要的属性; constructor
指向Person函数对象
*/
var p = new Person()
console.log(p.__proto__.constructor) // Person(){}
console.log(p.__proto__.constructor.name) // Person
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 重写原型对象
如果需要在原型上添加过多的属性,通常会重写整个原型对象
function Person() {}
// 在原有对象的原型上添加新的属性
Person.prototype.message = "Hello Person";
Person.prototype.info = { name: "zs", age: 20 };
Person.prototype.running = function () {};
Person.prototype.eating = function () {};
console.log(Person.prototype);
console.log(Object.keys(Person.prototype)); // ['message', 'info', 'running', 'eating'], constructor属性没有被枚举出来
// 直接赋值一个新的对象
Person.prototype = {
message: "Hello Person",
info: { name: "ls", age: 18 },
running: function () {},
eating: function () {},
// constructor: Person // 手动设置将constructor指向Person函数, 否则Person函数中的constructor将向上一层的原型链中查找指向Object
};
console.log(Object.keys(Person.prototype)) // ['message', 'info', 'running', 'eating', 'constructor'], 这里constructor属性被枚举出来了
// 手动将constructor属性设置为默认的配置
Object.defineProperty(Person.prototype,"constructor", {
configurable: true,
enumerable: false,
writable: true,
value: Person
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获取constructor
属性;而当给prototype
重新赋值了一个对象,那么这个新对象的constructor
属性会指向Object
构造函数,而不是Person
构造函数了
# 原型对象的constructor
如果希望constructor
指向Person,那么可以手动添加
虽然手动添加这种方式可以实现,但是也会造成constructor
的[[Enumerable]]
特性被设置为true
- 默认情况下原生的constructor属性是不可枚举的
- 解决这个问题就需要用到
Object.defineProperty()
函数
// 直接赋值一个新的对象
Person.prototype = {
message: "Hello Person",
info: { name: "ls", age: 18 },
running: function () {},
eating: function () {},
// constructor: Person // 手动设置将constructor指向Person函数, 否则Person函数中的constructor将向上一层的原型链中查找指向Object
};
console.log(Object.keys(Person.prototype)) // ['message', 'info', 'running', 'eating', 'constructor'], 这里constructor属性被枚举出来了
// 手动将constructor属性设置为默认的配置
Object.defineProperty(Person.prototype,"constructor", {
configurable: true,
enumerable: false,
writable: true,
value: Person
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建对象--构造函数和原型组合
当在一个构造函数上创建对象时,有一个弊端,会创建出重复的函数;如果想要让所有的对象共享这些函数,那么可以将这些函数放到Person.prototype
对象上
function Person(name, age, sno) {
this.name = name;
this.age = age;
this.sno = sno;
}
Person.prototype.running = function(){
console.log(this.name + " running"); // 这里的this是一个隐式绑定
}
var p1 = new Person("zs", 18, 111);
var p1 = new Person("ls", 20, 112);
2
3
4
5
6
7
8
9
10
11
# 面向对象的特性--继承
面向对象有三大特性:封装、继承、多态 (抽象【第四大特性】)
- 封装:将属性和方法封装到一个类中可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少代码量,也是多态的前提
- 多态:不同的对象在执行时表现出不同形态
# JavaScript原型链
如果要从一个对象中获取属性,如果当前对象中没有该属性就会到它的原型上面去获取
var info = {}
// 相当于
var info = new Object()
console.log(info.__proto__ === Object.prototype) // true
2
3
4
var obj = {
name: 'zs',
age: 18
}
console.log(obj.message);
2
3
4
5
由上面的查找过程获取灵感进行代码改造
var obj = {
name: 'zs',
age: 18
}
console.log(obj.message);
obj.__proto__ = {
message: 'Hello aaa'
}
obj.__proto__.__proto__ = {
message: 'Hello bbb'
}
obj.__proto__.__proto__.__proto__ = {
message: 'Hello ccc'
}
console.log(obj.message)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Object 的原型
原型链是否有尽头呢,比如上面的代码
console.log(obj.__proto__.__proto__.__proto__.__proto__) // null
最终的打印结果是[Object: null prototype]{}
- 这个就是这个原型的最顶层的原型了
- 从Object直接创建出来的对象的原型都是
[Object: null prototype]{}
[Object: null prototype]{}
的特殊性
- 该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了
- 该对象上有很多默认的属性和方法
Object是所有类的父类:原型链最顶层的原型对象就是Object的原型对象
# 通过原型链实现方法继承
function Person(name, age) {
(this.name = name), (this.age = age);
}
Person.prototype.running = function () {
console.log("running");
};
Person.prototype.eating = function () {
console.log("eating");
};
function Student(name, age, sno, score) {
this.name = name;
this.age = age;
// 上面两行代码这里不能删除,否则打印的对应的属性值就是父类中对应的属性值
this.sno = sno;
this.score = score;
}
console.log(Student.prototype.constructor)
// 方式一:父类的原型直接赋值给子类的原型
// 这种方式是错误的
// 缺点:父类和子类共享一个原型对象,修改了任意一个,另外一个也会跟着被修改
// Student.prototype = Person.prototype;
// 方式二:创建一个父类的实例对象(new Person()),用这个实例对象来作为子类的原型对象
var p = new Person("ls", 18);
Student.prototype = p;
Student.prototype.studying = function () {
console.log("studying...");
};
var stu1 = new Student("zs", 18, 111, 100);
stu1.running();
console.log(stu1.name, stu1.age, stu1.sno)
p.running()
console.log(Student.prototype.constructor)
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
# 原型链继承的弊端
通过原型链实现继承有一个很大的弊端:某些属性其实是保存在p(父级)对象上的
- 通过直接打印是看懂不到这个属性的
- 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
- 不能给Person传递参数,让每个新建的子对象有自己的属性,因为这个对象是一次性创建的,没办法定制化
# 借用构造函数实现属性继承
借用继承的做法非常简单:在子类构造函数内部调用父类构造函数
- 因为函数可以在任意时刻被调用
- 通过
call()
和apply()
方法也可以在新创建的对象上执行构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, sno, score) {
Person.call(this, name, age)
// this.name = name;
// this.age = age;
this.sno = sno;
this.score = score;
}
var p = new Person("ls", 18);
Student.prototype = p;
var stu1 = new Student("zs", 18, 111, 100);
console.log(stu1);
console.log(p);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 借用构造函数继承的问题
组合继承是JavaScript最常用的继承模式之一
组合继承存在的问题
- 组合继承最大的问题就是无论在什么情况下都会调用两次父类构造函数
- 一次在创建子类原型的时候
- 另一次在子类构造函数内部(每次创建子类实例的时候)
- 所有的子类实例事实上会拥有两份父类属性
- 一份在当前的实例(
person
)本身当中,另一份在子类对应的原型对象(person.__proto__
)中 - 这两份无需担心访问问题,因为默认一定是访问实例本身这一部分
# 原型式继承函数
原型式继承这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的一篇文章说起Prototypal Inheritance in JavaScript
在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的
下JavaScript想实现继承的目的:重复利用另外一个对象的属性和方法
/*
需要满足的条件
1.必须创建出来一个对象
2.这个对象的隐式原型必须指向父类的显式原型
3.将这个对象赋值给子类的显式原型
*/
function Person() {}
function Student() {}
// 之前的做法
var p = new Person();
Student.prototype = p;
// 方案一
var obj = {};
// obj.__proto__ = Person.prototype // __proto__存在一定的兼容性问题,尽量不使
Object.setPrototypeOf(obj, Person.prototype);
Student.prototype = obj;
// 方案二
function F() {}
F.prototype = Person.prototype;
Student.prototype = new F();
// 方案三
var obj = Object.create(Person.prototype);
Student.prototype = obj;
// 工具函数封装
// 创建对象过程
function createObject(o) {
function F() {}
F.prototype = o;
return new F();
}
// 将Subtype和Supertype联系在一起
// 寄生式函数
function inherit(Subtype, Supertype) {
// 不考虑兼容性问题的情况下
// Subtype.prototype = Object.create(Supertype.prototype);
// 兼容性问题处理
Subtype.prototype = createObject(Supertype.prototype);
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype,
});
}
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
# 寄生式继承函数
寄生式继承
- 寄生式继承是与原型式继承紧密相关的一种思想,由道格拉斯·克罗克福德(Douglas Crockford)提出和推广
- 寄生式继承的思路是结合原型类继承和工厂模式的是一种方式
- 即创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后将这个对象返回
# 寄生组合式继承
寄生组合继承的代码
// 工具函数封装
// 创建对象过程
function createObject(o) {
function F() {}
F.prototype = o;
return new F();
}
// 将Subtype和Supertype联系在一起
// 寄生式函数
function inherit(Subtype, Supertype) {
// 不考虑兼容性问题的情况下
// Subtype.prototype = Object.create(Supertype.prototype);
// 兼容性问题处理
Subtype.prototype = createObject(Supertype.prototype);
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype,
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 对象方法的补充
- hasOwnProperty
- 对象是否有某一个属于自己的属性(这个属性在自己本身上,不在原型上)
- in/for in 操作符
- 判断某个属性是否在某个对象或对象的原型上
- instanceof
- 用于检测构造函数(Person, Student类)的
prototype
,是否出现在某个实例对象的原型链上
- 用于检测构造函数(Person, Student类)的
- isPrototypeOf
- 用于检测某个对象,是否出现在某个实例对象的原型链上
var obj = {
name: "zs",
age: 18,
};
var info = createObject(obj);
info.address = "中国";
info.intro = "hello";
console.log(info.name, info.address);
console.log(info);
// hasOwnProperty
// console.log(info.hasOwnProperty("name")) // false
// console.log(info.hasOwnProperty("address")) // true
// in/for in
console.log("name" in info); // true
console.log("address" in info); // true
// for in 遍历不仅仅是自己身上的属性,也包括原型对象上的属性
for (var key in info) {
console.log(key);
}
// instanceof
function Person() {}
function Student() {}
inherit(Student, Person);
var stu = new Student();
console.log(stu instanceof Student); // trud
console.log(stu instanceof Person); // true
console.log(stu instanceof Object); // true
console.log(stu instanceof Array); // false
// isPrototypeOf
console.log(Student.prototype.isPrototypeOf(stu))
console.log(Person.prototype.isPrototypeOf(stu))
// 可以用于判断对象之间的继承关系
console.log(obj.isPrototypeOf(stu))
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
# 创建对象的内存表现
/* function Person() {}
var p = new Person();
console.log(p.__proto__);
console.log(Person.prototype)
console.log(p.__proto__ === Person.prototype) */
var obj = {}; // 相当于是 new Object() 创建出来的对象
console.log(obj.__proto__ === Object.prototype); // true
function foo() {} // 实际是new Function()创建出来的实例对象
console.log(foo.name, foo.length);
console.log(foo.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
2
3
4
5
6
7
8
9
10
11
12
结论
- p是Person的实例对象
- obj是Object的实例对象
- Function/Object/foo是Function的实例对象
- 原型对象那个默认创建时,隐式原型都是指向Object的显式原型的(Object的隐式原型指向null)
- Object是Person/Function的父类
# 构造函数中的类方法 实例方法
function Person(name, age) {
this.name = name;
this.age = age;
}
// 添加到Person原型上的方法也称为实例方法
Person.prototype.running = function () {
console.log("running...");
};
// 类属性
Person.total = 100;
// 添加到Person对象本身的方法称之为类方法
Person.randomPerson = function () {
return new Person("zs", 18);
};
2
3
4
5
6
7
8
9
10
11
12
13
14