# 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: ƒ}
1
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));
1
2
3
  • 可以通过 对象.__proto__ 获取到原型对象,但是千万不要通过 对象.__proto__ = value 来修改原型对象,会导致问题;
  • 还可以通过 Object.getPrototypeOf(obj) 的方式来获取原型对象。

JavaScript中为什么需要原型呢?

  1. 将方法定义在原型上,所有对象可以共享同一份方法,而不是每个实例都创建独立的副本,这样可以节省内存。

所以同一个类的多个对象的原型对象指向的是同一个对象,举个栗子:

class Person { 
  constructor(name) {
    this.name = name;
  }     
}

let p1 = new Person('Doubi');
let p2 = new Person('Niubi');

console.log(p1.__proto__ === p2.__proto__)  // true
1
2
3
4
5
6
7
8
9
10
  1. 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);
1
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 {}
1
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}
1
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
1
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
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • Person.prototypeperson.__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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关系如下:

通过上面的原型关系形成的链条,就是原型链:

person --> Person.prototype --> Object.prototype --> null
1

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
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

形成的原型链如下:

student --> Student.prototype --> Person.prototype --> Object.prototype --> null
1

结构如下图:

当在访问对象属性或方法的时候,如果对象自身没有,会去原型对象查找,如果原型对象没有,那么回去原型对象的原型对象查找。也就是会沿着原型链一直查找,知道找到 Object 的原型对象,Object 原型对象为 null,停止查找。

所以 Student 类继承自 Person 类,当你访问 student.sayHello() 时,JavaScript 执行如下操作:

  1. student 对象本身查找 → ❌ 没有
  2. 查找 student.__proto__(即 Student.prototype) → ❌ 没有
  3. 查找 student.__proto__.__proto__(即 Person.prototype) → ✅ 找到
  4. 如果还没找到,继续找 Person.prototype.__proto__(即 Object.prototype)...
  5. 直到 __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
1
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
1
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
1
  • 结果为 true ,也就是说:**Function 自己是 Function 的实例!**而在 JavaScript 中,Function 函数本身是唯一一个自己是自己创建的函数。

下面的代码执行,也确实为 true

console.log(Function instanceof Function);    // true
1

这样就形成了一个逻辑自洽,也就是所有函数(包括 Function)的 __proto__ 都指向 Function.prototype,保持原型链的统一性。

这里还有一个点需要注意,Object 也是构造函数,所以 Object.__proto__ 也是指向的 Function.prototype

console.log(Object.__proto__ === Function.prototype);  // true
1
  • 注意,不是 Object.prototypeObject.prototype 指向 null

不好意思,问题还没完!

上面说了函数的 __proto__ 属性都指向 Function 构造函数的 prototype 属性。

举个栗子:

class Person {
}

console.log(Person.__proto__ === Function.prototype); // true
1
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
1
2
3
4
5
6
7
8
9
10
11
  • 通过上面的代码看,又不是那么回事。

对于普通的构造函数,比如 PersonPerson 是一个函数,而所有函数都是 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
1
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
1
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
1
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
1
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);
1
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'));
1
2
3
4
5