# JavaScript教程 - 9 面向对象
# 9.5 原型简介
讲解原型之前,先看一个栗子。
class Person {
name = 'Doubi';
sayHello() {
console.log('Hello');
}
sleep = function() {
console.log('sleep');
}
}
let person = new Person();
person.age = 13; // 添加属性
person.eat = function() { // 添加方法
console.log('eat');
}
console.log(person); // Person {name: 'Doubi', age: 13, sleep: ƒ, eat: ƒ}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 在上面的代码中,通过 Person 类创建了 person 对象,并给 person 对象添加了属性和方法。
但是在打印 person 对象的时候,发现并没有 sayHello()
方法,这是为什么呢?
因为对象中存储属性的区域实际有两个:
对象自身
在类中通过赋值的方式(
x = value
)添加的属性,位于对象自身中;直接通过
对象.属性 = value
的方式添加的属性,位于对象自身中。原型对象
对象中还有一些内容,会存储在别的对象中,也就是原型对象,也就是说原型对象中也会存储对象的属性和方法,当我们通过对象来访问属性或方法的时候,会先在对象自身中寻找,如果找不到,就会去原型对象中寻找。
所以上面通过
sayHello(){}
添加的方法,实际是添加到原型对象中了。
在对象中,会有一个属性存储原型对象的引用,这个属性叫 __proto__
:
我们可以通过如下方式访问原型对象:
let person = new Person();
console.log(person.__proto__);
console.log(Object.getPrototypeOf(person));
2
3
- 可以通过
对象.__proto__
获取到原型对象,但是千万不要通过对象.__proto__ = value
来修改原型对象,会导致问题; - 还可以通过
Object.getPrototypeOf(obj)
的方式来获取原型对象。
JavaScript中为什么需要原型呢?
- 将方法定义在原型上,所有对象可以共享同一份方法,而不是每个实例都创建独立的副本,这样可以节省内存。
所以同一个类的多个对象的原型对象指向的是同一个对象,举个栗子:
class Person {
constructor(name) {
this.name = name;
}
}
let p1 = new Person('Doubi');
let p2 = new Person('Niubi');
console.log(p1.__proto__ === p2.__proto__) // true
2
3
4
5
6
7
8
9
10
- JavsScript的继承是使用原型实现的,这个下面细讲。
# 9.6 原型链与继承
# 1 构造函数
在前面我们讲创建对象的方式,有以下三种方式:
// --方式1
let person1 = {
name: "Doubi",
age: 13,
};
// --方式2
let person2 = new Object();
person2.name = "Doubi";
person2.age = 13;
// --方式3
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let person3 = new Person("Doubi", 13);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在 ES6 之前,创建对象是通过构造函数来创建的,class
是 ES6 中引入的,class
其实是构造函数的语法糖(更简洁更易读的语法),而 class
实现的继承底层仍是原型,新的 class
写法只是让原型的写法更加的清晰、更像面向对象编程的语法而已。
所以既然 class 是语法糖,那就先从构造函数讲起。
创建一个 Person 的构造函数,用来创建 Person 对象:
// 函数
function Person() {
}
let person = new Person(); // 通过 new 使用
console.log(person); // Person {}
2
3
4
5
6
- 上面的
Person()
函数就是一个构造函数,构造函数和普通的函数没有本质区别,只是调用方式不同。当我们使用new
来调用一个函数,那么它就是被当做构造函数,返回的就是一个对象。
当然我们想把一个函数作为构造函数来创建对象,那么会在对象中添加属性、方法,是我们使用函数的方式不同,才区分了构造函数和普通函数:
// 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
let person = new Person('Doubi', 13);
console.log(person); // Person {name: 'Doubi', age: 13}
2
3
4
5
6
7
8
使用构造函数创建的实例,都有一个 constructor
属性指向构造函数:
// 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
let p1 = new Person("Doubi", 13);
let p2 = new Person("Niubi", 18);
console.log(p1.constructor === Person); // true
console.log(p2.constructor === Person); // true
console.log(p1.constructor === p2.constructor); // true
2
3
4
5
6
7
8
9
10
11
12
class
是构造函数的语法糖,那么使用类也是一样的:
// 构造函数
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let p1 = new Person("Doubi", 13);
let p2 = new Person("Niubi", 18);
console.log(p1.constructor === Person); // true
console.log(p2.constructor === Person); // true
console.log(p1.constructor === p2.constructor); // true
console.log(p1.hasOwnProperty("constructor")); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
但是上面 person1.hasOwnProperty("constructor")
判断对象是否有 constructor
返回的是 false
,也就是说实例本身是没有 constructor
属性的,这个 constructor
属性是继承自实例的原型对象。
也就是:p1.constructor -> p1.__proto__.constructor -> Person
。
下面来仔细说一下原型链。
# 2 原型链
在前面说到,一个对象是有一个 __proto__
属性,指向的是对象的原型对象。
但其实是构造函数通过 .prototype
属性指向了原型对象,在用 new
创建实例时,会自动把这个原型对象赋值给实例的 __proto__
,从而建立原型关系。
什么意思呢?
构造函数对象(普通函数也有)都会有一个属性 prototype
,它指向的就是原型对象,原型对象保存了该构造函数,以及定义的方法,而通过该构造函数生成的对象的 __proto__
属性,也指向了该原型对象。
举个栗子:
// 构造函数
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello");
}
}
let person = new Person("Doubi", 13);
console.log(person.__proto__ === Person.prototype); // true
console.log(person.__proto__.constructor === Person); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Person.prototype
和person.__proto__
指向的都是原型对象。
每个对象都有一个 __proto__
属性,那么原型对象的 __proto__
属性又指向谁呢?
Person 类没有父类,所以默认是继承 Object 类,所以 Person 构造函数的原型对象指向的是 Object 类的原型对象。
// 构造函数
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello");
}
}
let person = new Person("Doubi", 13);
console.log(person.__proto__ === Person.prototype); // true
console.log(person.__proto__.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
关系如下:
通过上面的原型关系形成的链条,就是原型链:
person --> Person.prototype --> Object.prototype --> null
Object 构造函数的原型对象( Object.prototype
)的 __proto__
属性值为 null
,也就是原型链的终点。
如果是 Student 类继承 Person 类:
// 人类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello");
}
}
// 学生类
class Student extends Person {
constructor(name, age) {
super(name, age)
}
study() {
console.log("Study");
}
}
let student = new Student("Doubi", 13);
student.sayHello(); // Hello
console.log(student.__proto__.__proto__ === Person.prototype); // true
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
形成的原型链如下:
student --> Student.prototype --> Person.prototype --> Object.prototype --> null
结构如下图:
当在访问对象属性或方法的时候,如果对象自身没有,会去原型对象查找,如果原型对象没有,那么回去原型对象的原型对象查找。也就是会沿着原型链一直查找,知道找到 Object 的原型对象,Object 原型对象为 null,停止查找。
所以 Student 类继承自 Person 类,当你访问 student.sayHello()
时,JavaScript 执行如下操作:
- 在
student
对象本身查找 → ❌ 没有 - 查找
student.__proto__
(即Student.prototype
) → ❌ 没有 - 查找
student.__proto__.__proto__
(即Person.prototype
) → ✅ 找到 - 如果还没找到,继续找
Person.prototype.__proto__
(即Object.prototype
)... - 直到
__proto__ === null
,查找结束。
所以说 JavaScript 使用原型链来实现继承,__proto__存在的意义在于为原型链查找提供方向。
在以前,我们经常在原型上添加方法,这样实例对象就拥有这个方法了。
例如给字符串扩展一个方法,将字符串首字母大写,其他字母小写的方法,就可以在 String.prototype
原型对象上进行扩展:
// 给字符串原型扩展一个方法
String.prototype.toCapitalize = function () {
if (this.length === 0) {
return "";
}
else {
return this[0].toUpperCase() + this.slice(1).toLowerCase(); // slice(1)表示从index为1的位置截取字符串
}
};
console.log("hEllO".toCapitalize()); // Hello
2
3
4
5
6
7
8
9
10
11
- 给
String.prototype
原型扩展方法后,字符串实例就有这个方法了,就可以直接使用了
当然这种方式现在不太推荐,你添加我添加,很容易覆盖或冲突,推荐使用工具类的方式。
# 9.7 函数原型
上面说了构造函数(类)会有 prototype
属性,其实普通函数也有这个属性。
但是只有当这个函数被当作构造函数使用(用 new
调用)时,prototype
才真正起作用。如果函数从来不用于 new
,那么它的 prototype
没有什么实际意义。
在 JavaScript 中,除了基础数据类型,其他的类型都是对象,所以函数也对象。是对象自然有 __proto__
属性指向原型对象。
那么问题来了,函数的 __proto__
属性指向谁呢?其实这个问题的关键就是函数是谁的实例?
所有函数本身都是 Function
的实例,所以看一下下面的代码:
// 函数
function Test() {
console.log('test');
}
console.log(typeof Test); // function
console.log(Test instanceof Function); // true
console.log(Test.__proto__ === Function.prototype); // true
2
3
4
5
6
7
8
- 通过上面的例子可以看出,函数的类型是
function
其实还是对象,只是为了区分普通对象。 - 函数本身是
Function
的实例,所以自然Function
肯定是一个构造函数,那么Function
的实例(也就是所有函数)的__proto__
属性都指向Function
构造函数的prototype
属性所指向的原型,所以Test.__proto__ === Function.prototype
。 - 类就是构造函数,就是函数,所以类也是一样的。
关系图如下:
问题还没完!
普通函数是对象,有 __proto__
属性,Function
也是构造函数,也是对象,也有 __proto__
属性,那么 Function
函数的 __proto__
属性又指向谁呢?
看一下下面的代码:
console.log(Function.__proto__ === Function.prototype); // true
- 结果为
true
,也就是说:**Function 自己是 Function 的实例!**而在 JavaScript 中,Function
函数本身是唯一一个自己是自己创建的函数。
下面的代码执行,也确实为 true
:
console.log(Function instanceof Function); // true
这样就形成了一个逻辑自洽,也就是所有函数(包括 Function
)的 __proto__
都指向 Function.prototype
,保持原型链的统一性。
这里还有一个点需要注意,Object 也是构造函数,所以 Object.__proto__
也是指向的 Function.prototype
。
console.log(Object.__proto__ === Function.prototype); // true
- 注意,不是
Object.prototype
,Object.prototype
指向null
。
不好意思,问题还没完!
上面说了函数的 __proto__
属性都指向 Function
构造函数的 prototype
属性。
举个栗子:
class Person {
}
console.log(Person.__proto__ === Function.prototype); // true
2
3
4
- 通过上面的代码看,确实那么回事。
让 Student 类继承 Person 类呢?
class Person {
}
class Student extends Person {
}
console.log(Object.__proto__ === Function.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(Student.__proto__ === Function.prototype); // false
console.log(Student.__proto__ === Person); // true
2
3
4
5
6
7
8
9
10
11
- 通过上面的代码看,又不是那么回事。
对于普通的构造函数,比如 Person
,Person
是一个函数,而所有函数都是 Function
的实例,所以 Person.__proto__
指向 Function.prototype
。但让 class Student extends Person
时,Student
的 __proto__
不再指向 Function.prototype
,而是指向它的父类 Person
。
所以其实这里有两条原型链:
- 对象实例原型链:
student → Student.prototype → Person.prototype → Object.prototype → null
- 构造函数原型链:
Student → Person → Function.prototype → Object.prototype → null
但这里还有一个让人迷惑的地方:
看一下下面的代码:
class Person {
}
console.log(Object.__proto__ === Function.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
2
3
4
5
和下面的代码比较一下:
class Person extends Object {
}
console.log(Object.__proto__ === Function.prototype); // true
console.log(Person.__proto__ === Function.prototype); // false
console.log(Person.__proto__ === Object); // true
2
3
4
5
6
- 将 Person 继承 Object 和不继承 Object 是有区别的。
在其他的面向对象语言中,经常有这样的规则:一个类如果没有写明继承哪个类,那么默认都是继承自Object类,所以最终的结果就是Object类是所有类的父类!
但是这句话在 JavaScript 只能对一半,为什么?
因为 JavaScript 中的继承是使用原型链实现的,不是基于类。从对象实例的原型上讲,Object.prototype 确实是所有对象实例的终极原型。所以从这个角度讲,一个类如果没有写明继承哪个类,那么默认都是继承自 Object 类,从对象实例的角度上讲,也算正确。
但 Object(构造函数)不是所有类的构造器的父类。JavaScript 中所有函数(包括类)默认都是 Function
的实例,Function
才是**所有函数(包括类)**的“构造函数之父”。所以 Person.__proto__
指向 Function.prototype
。
当使用 Person extends Object
时,JavaScript 引擎才会把类的构造器的原型指向 Object
,实现“构造器的继承” 。
不过一般情况下,我们的类也不需要 extends Object
,除非你想要使用 Object
中的静态方法,因为继承 Object 才能拥有,这种情况很少。
# 9.8 原型总结
差不多了,通过上面的讲解应该对原型有了解了,确实有点绕。
下面总结一下,画个关系图:
最后再提醒一下,类就是构造函数的语法糖,也就是构造函数。
你说上面讲这些,鸡毛用?
说实话对写代码用处不大,只是搞清楚了底层的原理,面试会用!呵呵~
# 9.9 旧类
上面讲到,类就是构造函数,构造函数和普通函数本质上是一样的,只是调用的方式不同。如果使用 new
调用就是构造函数,以普通函数方式调用,就是普通函数。
下面再来说一下,在没有 class 的时候,如何实现定义类,并在类中定义属性、方法,以及实现类的继承。
举个栗子,Student
类继承 Person
类,代码如下:
# 9.10 实例判断
前面讲判断一个对象是否是一个类的实例,使用 instanceof
操作符。
但是 instanceof
是使用原型链来判断的,a instanceof B
会判断,在对象 a
的原型链上,是否能找到 B.prototype
,则 a 是 B 类的实例。
这样如果 A 类是 B 类的子类,那么 A 类的对象就是 B 类的实例。
举个栗子:
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
2
3
4
5
6
7
- 上面 Dog 类继承 Animal 类,所以 dog 对象也是 Animal 的实例。
可不可以判断一个对象是一个类的实例,但不是父类的实例呢?
很简答,直接使用 dog.__proto__ === Dog.prototype
判断即可。
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog.__proto__ === Dog.prototype); // true
console.log(dog.__proto__ === Animal.prototype); // true
2
3
4
5
6
7
# 9.11 属性判断
在对象章节,可以通过 in
操作符判断一个对象中是否有某个属性。但是 in 操作符检查的时候,无论是对象自身还是在原型中,都会返回true。
举个栗子:
class Person {
name = "Doubi";
sayHello() {
console.log("Hello");
}
}
let person = new Person();
console.log('name' in person);
console.log('sayHello' in person);
2
3
4
5
6
7
8
9
10
11
- 在上面的代码中,sayHello() 方法是在原型对象中的,但是
in
操作符也会返回true。
如果指向判断是否是对象自身的属性,可以使用如下方式:
// 新版,推荐,Object提供的静态方法
console.log(Object.hasOwn(person, 'sayHello'));
// 旧版,不推荐,Object原型提供的方法
console.log(person.hasOwnProperty('sayHello'));
2
3
4
5