原型和原型链是前端老生常谈的问题,以前常常在各种面试题中看到,我自己也背过不少次,但总是感觉磕磕巴巴的,明显没有真正理解这一概念,最近又要面试,再次看到这个问题,突然有些豁然开朗的感觉,因为前阵子参加 game jam 时,做了一个小游戏,不可避免地用到了继承,再次看到原型链时才恍然大悟,原来原型链要解决的其实是 JavaScript 中对象继承的问题。这篇文章主要是结合一下自己以前对面向对象的理解,来理清一下对原型和原型链的认识。
原型和原型链
先了解一下面向对象然后明确一些定义:JavaScript 是一种基于原型的语言,而不是基于类的语言,这一点和 Java、C# 这些传统的面向对象编程语言是不同的,但这不意味着 JavaScript 不是面向对象语言,回忆一下上学时学的面向对象的三大特性:封装、继承、多态,JavaScript 均可以实现,但今天我们主要关注的是继承,而继承的实现就是通过原型和原型链。
原型
直接说定义未免太过抽象,我们先看一个简单的例子:
/* by 01130.hk - online tools website : 01130.hk/zh/formatperl.html */
const obj = {
name: "Alice",
greet: function () {
console.log("Hello, " + this.name);
},
};
我们简单创建了一个对象obj,它有一个属性name和一个方法greet,当我们调用obj.greet()时,它会输出Hello, Alice,或者我们可以输出obj.name来获取属性值,但当我们在 obj 后面加上一个点,比如obj.,这时会弹出一个提示框,显示出的属性和方法远不止这两个,这些属性和方法是从哪里来的呢?

试着访问其中一个,比如toString方法:
/* by 01130.hk - online tools website : 01130.hk/zh/formatperl.html */
console.log(obj.toString()); // [object Object]
结果不是undefined,toString方法并不是我们在创建obj时定义的,那么它是从哪里来的呢?这就涉及到原型和原型链的概念了,JavaScript 中的数据类型分为值类型和引用类型,而一切引用类型都是对象,原型和原型链正是服务于对象的概念,同时,对象由属性组成,而 js 的每个对象都有一个内部属性__proto__,除此之外,函数对象都有一个属性prototype,__proto__会指向一个prototype对象。

具体是如何指向的,我们继续通过代码示例来看:
function Person(name) {
this.name = name;
}
const person1 = new Person("Alice");
Person是一个函数对象,person1是一个实例对象,两者都有各自的__proto__属性,同时Person函数对象还有一个prototype属性,关系如下:
Person.__proto__:指向Function.prototype,因为Person是一个函数对象Person.prototype:值是一个对象,包含了通过Person创建的实例对象所共享的属性和方法person1.__proto__:指向Person.prototype,表示person1实例对象继承自Person.prototype
我们一般说对象的__proto__属性所指向的对象就是它的原型,在这个例子中,person1.__proto__指向Person.prototype,所以Person.prototype就是person1的原型,而Person.__proto__指向Function.prototype,所以Person的原型就是Function.prototype。
也许还不够直观,我们把上面的内容转成图像:

我认为这样足够清晰了,而它们的关系形成了一串链条,这就是原型链,所以上面的例子中在调用obj.toString()时,JavaScript 引擎会先在obj对象上查找toString方法,如果没有找到,就会沿着原型链向上查找,最终在Object.prototype上找到了这个方法,原型链的详细过程我们之后再说,关于原型还有一些问题要解决,我们已经知道了__proto__是一个属性,并指向一个prototype对象,而prototype的值是什么呢?
prototype属性的值是一个对象,这个对象包含了通过该函数创建的实例对象所共享的属性和方法。默认情况下,这个对象只有一个constructor属性,指向构造函数本身。
又出现了一个陌生的概念——构造函数,接下来我们先理解构造函数。
构造函数
创建一个对象最常见的方式有两种,上文中的两个例子刚好对应这两种方式:
- 对象字面量
const obj = { name: "Alice" };
- 构造函数
构造函数是用来创建对象的函数,通常以大写字母开头,以区分普通函数和构造函数,当我们使用new操作符调用一个函数时,这个函数就被当作构造函数来使用。
function Person(name) {
this.name = name;
}
const person1 = new Person("Alice");
这两种方式有什么区别呢?对象字面量创建的对象是一个普通对象,而使用构造函数创建的对象是通过new操作符实例化出来的对象,我们前面说过,所有的对象都有__proto__属性,我们可以先比较一下这两种方式创建的对象的__proto__属性:
const obj = { name: "Alice" };
function Person(name) {
this.name = name;
}
const person1 = new Person("Alice");
console.log(obj.__proto__ === Object.prototype); // true
console.log(person1.__proto__ === Person.prototype); // true
可以看到,obj的__proto__指向Object.prototype,而person1的__proto__指向Person.prototype,这说明通过对象字面量创建的对象继承自Object.prototype,而通过构造函数创建的对象继承自构造函数的prototype属性。
构造函数的作用
说了那么多,我们为什么要通过构造函数来创建对象呢?有两个主要的作用:
- 复用结构
使用字面量创建对象时,每个对象都要手动创建,无法复用结构,而使用构造函数可以定义一个模板,通过new操作符创建多个实例对象,复用结构,其实就是面向对象编程中的类的概念。
const person1 = { name: "Alice", age: 25 };
const person2 = { name: "Bob", age: 30 };
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
- 继承
构造函数的另一个重要作用是实现继承,通过构造函数创建的实例对象可以继承构造函数prototype属性上的方法和属性,从而实现代码的复用。
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
Person.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
console.log(person1.__proto__);
person1.sayHello(); // "Hello, I'm Alice"
person2.sayHello(); // "Hello, I'm Bob"
这就引出了prototype属性的作用,prototype属性是函数对象特有的属性,它的作用就是为实例对象(person1、person2)提供原型对象,它告诉引擎应该继承哪个对象的属性和方法,实际上就是一个“实例原型指针”。
我们可以在控制台打印Person.prototype看看(这里用的是前面的例子,所以只有一个入参 name):

可以看到,Person.prototype默认有一个constructor属性,指向Person函数本身,我们可以在Person.prototype上添加属性和方法,这些属性和方法会被所有通过Person构造函数创建的实例对象继承。

回忆一下,我们前面说过实例对象的__proto__属性指向构造函数的prototype属性,也就是说,person1.__proto__ === Person.prototype,所以当我们调用person1.sayHello()时,JavaScript 引擎会先在person1对象上查找sayHello方法,如果没有找到,就会沿着原型链向上查找,最终在Person.prototype上找到了这个方法,这就是原型链与继承。

上图所示,person1对象的__proto__和Person.prototype是同一个对象,值得注意的是打印出来的对象中没有__proto__而是[[Prototype]],这是因为在控制台打印对象时,浏览器会将__proto__属性显示为[[Prototype]],但它们实际上是同一个东西,访问对象原型其实也不建议直接使用__proto__,而是使用Object.getPrototypeOf()方法。
new 操作符的作用
现在我们说起 new 的作用时,应该也不那么晦涩难懂了,当我们使用new Person("Alice")创建实例对象时,实际上发生了以下几件事:
- 创建一个新的空对象。
- 将这个新对象的
__proto__属性指向构造函数的prototype属性。 - 将构造函数的
this指向这个新对象,并执行构造函数的代码。 - 如果构造函数没有显式返回一个对象,则返回这个新对象。
所以,new操作符的作用就是创建一个新的对象,并将其原型指向构造函数的prototype属性。
常见的new操作符手写题可以参考下面的代码:
function myNew(Con, ...args) {
let obj = Object.create(Con.prototype);
let result = Con.apply(obj, args);
return typeof result === "object" ? result : obj;
}
Object.create()方法创建一个新的对象,并允许你指定一个将被用作新对象原型的对象。
原型链
现在我们可以继续说原型链了,原型链是由对象的__proto__属性和构造函数的prototype属性组成的一条链条,它定义了对象之间的继承关系,当我们访问一个对象的属性或方法时,JavaScript 引擎会沿着这条链条向上查找,直到找到该属性或方法为止,或者到达链条的终点(即null),我们把上面的那张图完善一下:

好像复杂很多,不要急,我们还是一步步拆解。
- 包含和指向
包含关系比较简单,前面我们说过所有对象都有__proto__属性,而函数对象还有prototype属性,所以图中我用虚线表示,我们不用过多关注。prototype是一个对象,因此它也有__proto__属性,这个前面的图中没有体现,这里加上了。
- Function.prototype
Function是 JavaScript 中的一个内置函数对象,所有函数对象的__proto__属性都指向Function.prototype,包括Function本身和Object(图中为了不让线条重叠,Object.__proto__的指向用了折线)。
- Object.prototype
Object是 JavaScript 中的另一个内置函数对象,所有普通对象的__proto__属性最终都会指向Object.prototype,包括通过对象字面量创建的对象和通过构造函数创建的实例对象(图中为了不让线条重叠,person1.__proto__的指向用了折线)。
- 终点 null
Object.prototype的__proto__属性指向null,表示原型链的终点,当 JavaScript 引擎沿着原型链查找属性或方法时,如果到达了null,就会停止查找,并返回undefined。
最后我们可以在控制台中查看一下person1.__proto__:

第一个框是person1的构造函数Person的prototype对象,第二个框是Person.__proto__,也就是Function.prototype,第三个框是Function.prototype.__proto__,也就是Object.prototype,最后是person1.__proto__,指向的是Object.prototype,这与我们上面的图是一致的。
原型和原型链的其他作用
通过上面的分析,我们已经了解了原型和原型链的基本概念和作用,除了实现继承之外,原型和原型链还有其他一些作用:
- 共享属性和方法,节省内存
将多个实例共用的属性或方法挂载到构造函数的 prototype 上,所有实例会通过原型链共享这些资源,避免每个实例都重复创建相同的方法(大幅节省内存),例如上面的Person.prototype.sayHello方法。
- 扩展内置对象的功能
通过原型链,我们可以为内置对象(如Array、String、Object等)添加新的方法,从而扩展它们的功能。例如,我们可以为Array.prototype添加一个新的方法:
Array.prototype.first = function () {
return this[0];
};
const arr = [1, 2, 3];
console.log(arr.first()); // 1
甚至你可以重写内置方法,比如你觉得 JS 原生的数组排序方法不符合你的需求,你可以重写 Array.prototype.sort,但不建议这样做,显然会带来一些严重的问题。其实这就是面向对象中多态的体现,不同的对象可以有不同的实现方式。
- 精准检测对象类型
利用 Object.prototype.toString 方法(原型链顶层的方法),可以更精准地判断对象的原生类型(比 typeof 更可靠)—— 因为不同内置对象的原型链上,toString 方法被重写为 “返回自身类型”,而 Object.prototype.toString 能返回最原始的类型标识。
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call({})); // [object Object]
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
console.log(Object.prototype.toString.call(Person)); // [object Function]
console.log(Object.prototype.toString.call(person1)); // [object Object]
JavaScript 中的类
ES6 引入了类的概念,提供了一种更简洁和直观的方式来创建对象和处理继承。类实际上是基于原型和原型链实现的语法糖,背后仍然使用了构造函数和原型链机制。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
const person1 = new Person("Alice", 25);
person1.sayHello(); // "Hello, I'm Alice"
以上代码与我们之前使用构造函数创建对象的方式是等价的,类的constructor方法相当于构造函数,而类的方法(如sayHello)会被添加到类的prototype属性上,从而实现继承。
总结
回想起来,后端的继承知识其实我一直都没有忘,但是在学习 JavaScript 时却没有能融会贯通,这么多年面对原型的问题还是靠死记硬背,实际上完成这篇文章时发现工作中很多时候都能用过与原型有关的知识点,另外,也是靠 AI 的帮助,才让我把这些零散的知识点串联起来,对于一些拿不准的地方,AI 可以给出相对准确的解释,帮助非常大。
参考资料
- 彻底搞懂 JS 原型与原型链
- MDN - 对象原型
1155

被折叠的 条评论
为什么被折叠?



