- Published on
JavaScript 原型与原型链详解
- Authors
- Name
- 青雲
JavaScript是一种基于原型的语言,这意味着对象间的继承是通过原型(prototype)实现的。要理解JavaScript的工作方式,掌握原型和原型链的概念是至关重要的。
原型(prototype)
JavaScript的原型是一个内部属性,所有通过函数创建的对象都会有一个隐藏的属性[[Prototype]]
,在ECMAScript 5+的代码中通常通过__proto__
属性来访问。原型的主要用途是实现对象的继承:当试图访问一个对象的属性时,如果对象本身没有这个属性,那么JavaScript就会去它的原型中查找。
构造函数是普通的函数,用new
操作符调用时,它们将创建新的对象。在函数内部,this
关键字指向新创建的对象。所有函数(除了箭头函数)都有一个名为prototype
的属性,它是一个对象。当你用构造函数创建一个新对象时,这个新对象的内部[[Prototype]]
属性(即__proto__
)被赋值为构造函数的prototype
对象。
__proto__
属性(在ECMAScript 2015以前是非标准的,但许多环境支持)是对象的内部属性,它是对对象原型的引用。准确来讲,__proto__
是[[Prototype]]
内部属性的getter
和setter
,而[[Prototype]]
是真正的原型链接。
function Person(name) {
this.name = name;
}
// 将方法放在原型上,所有使用Person创建的实例都可以访问
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
const alice = new Person('Alice');
alice.sayHello(); // 访问原型链上的方法
console.log(alice.__proto__ === Person.prototype); // 输出 true
console.log(alice.__proto__);
Prototype
和__proto__
的联系
ConstructorFunction
表示任意的构造函数,有一个prototype
属性指向PrototypeObject
。PrototypeObject
表示构造函数prototype
属性指向的对象,有一个constructor
属性回指ConstructorFunction
。InstanceObject
表示通过new ConstructorFunction()
创建的实例,它有一个__proto__
属性链接到PrototypeObject
。
原型链
JavaScript的原型链是一种基于原型的继承机制,用于解析和查找对象属性和方法。在JavaScript中,几乎所有对象都是Object
的实例,继承自Object
的方法和属性。例如,数组对象继承自Array.prototype
,数组方法如.forEach
、.map
等都定义在这里,但所有数组实例也能访问Object.prototype
上的方法,比如.toString
和.hasOwnProperty
。
原型链的工作原理
当你尝试访问一个对象的属性时,JavaScript首先检查这个对象本身是否有这个属性。如果没有,它会查找这个对象的原型(也就是__proto__
属性或者通过Object.getPrototypeOf()
得到的对象),继续在这个原型上查找属性。如果仍然找不到,它会查找原型的原型,以此类推,直到找到该属性或者到达原型链的顶端——通常是Object.prototype
。 如果连Object.prototype
上也找不到所需的属性,那么结果就是undefined
,这说明该属性在原型链上不存在。这条搜索路径就形成了一条“链”,即我们所说的原型链。 所有对象的原型链最终都会指向Object.prototype
(JavaScript中几乎所有对象的基础原型),除非明确设置其原型为null
。Object.prototype
的原型是null
,表示没有更上一层的原型,原型链在此终结。
原型链的作用
原型链主要实现了两个方面的功能:
- 属性查找:当访问对象的属性时,如果对象本身不存在这个属性,JavaScript会沿着原型链向上查找,直到找到属性或到达原型链的末端。
- 实现继承:通过原型链,一个对象可以使用另一个对象的属性和方法,实现继承。创建新对象时,可以指定这个新对象的原型为另一个对象的实例,从而继承该对象的属性和方法。
示例
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
};
function Dog(name) {
Animal.call(this, name); // 调用父类构造函数
}
Dog.prototype = Object.create(Animal.prototype); // 继承
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
console.log(`${this.name} barks.`);
};
const dog = new Dog('Rex');
dog.speak(); // 输出: Rex barks
在这个示例中,Dog
类继承了 Animal
类。Dog.prototype
的原型是 Animal.prototype
,因此 dog
实例可以访问 Animal
的 speak
方法。
console.log(dog.__proto__); // Dog.prototype
console.log(dog.__proto__.__proto__); // Animal.prototype
console.log(dog.__proto__.__proto__.__proto__); // Object.prototype
console.log(dog.__proto__.__proto__.__proto__.__proto__); // null
原型链在现代JS框架中的应用
在现代 JavaScript 框架中,原型链的应用通常被抽象化,并且不像在原生 JavaScript 编程中那样明显。React 和 Vue 等框架隐藏了大多数直接操作原型链的需求,但底层仍然依赖于 JavaScript 的原型继承机制。
React
React 是一个使用 JavaScript 构建用户界面的库,早期版本中通过 React.createClass
方法创建类,并且有显式的原型链继承。然而,自从 ES6 引入了类语法后,React 推荐使用 ES6 类来创建组件。 在 ES6 类组件中,React 使用原型链来继承 React.Component
的方法,如 render
, setState
, forceUpdate
等,这是内部使用原型链的一个例子。 然而,React 还大量采用了组合而不是继承来代码重用,如高阶组件(HOC)、Render Props和Hooks。
class MyComponent extends React.Component {
// 组件方法可以使用继承自 React.Component 的方法
}
Vue
Vue 是一个渐进式 JavaScript 框架,专注于构建用户界面。Vue 的核心概念包括响应式系统、组件化等,这些概念在底层也使用了原型链。 组件实例在内部通过 Vue 构造器创建,其中部分原型链继承是由 Vue 构造器的 extend 方法实现的。Vue 组件的生命周期钩子、数据观察和事件系统等都是通过这种方式挂载到 Vue 实例上的。
import Vue from 'vue';
const MyComponent = Vue.extend({
data() {
return {
msg: 'Hello, World!'
};
},
methods: {
sayHello() {
console.log(this.msg);
}
}
});
const app = new MyComponent().$mount('#app');
在这个例子中,MyComponent
继承自 Vue
构造函数。通过 Vue.extend
创建的组件其实是通过原型链继承了 Vue.prototype
。
Vue 的响应式系统利用了 Object.defineProperty
和 Proxy
(Vue 3.x 中)来实现属性拦截和依赖追踪。这些响应式属性的 getter
和 setter
函数都定义在原型链上。
const data = {
message: 'Hello Vue'
};
const reactiveHandler = {
get(target, key) {
console.log('Getting key:', key);
return target[key];
},
set(target, key, value) {
console.log('Setting key:', key, 'to', value);
target[key] = value;
return true;
}
};
const proxy = new Proxy(data, reactiveHandler);
proxy.message; // 输出: Getting key: message
proxy.message = 'Hello React'; // 输出: Setting key: message to Hello React
在 Vue 中,当你定义一个响应式对象时,Vue 会在原型链上设置这些响应式属性的 getter
和 setter
,确保数据变动能够被捕捉并引发视图更新。
尽管现代 JavaScript 框架如 React 和 Vue 主要使用类、组件化等更高层的抽象,但原型链依然是它们背后实现多态和继承关系的基础。理解原型链不仅帮助我们更好地理解这些框架的原理,还能提升我们调试代码和进行性能优化的能力。例如,通过明确框架如何在原型链上查找方法和属性,我们可以更有效地组织代码,避免不必要的属性查找和性能开销。
面试实战
题一:什么是原型链?
答案:原型链是一种对象属性和方法的查找机制。每个 JavaScript 对象都有一个隐藏的、内部的[[Prototype]]
属性,通常作为 __proto__
外部属性存在。对象的 __proto__
指向它的原型对象,而这个原型对象又有它自己的原型,依此类推,直到一个对象的 __proto__
是 null
为止。这种结构形成了一条链,称为原型链。
题二:如何创建对象的原型链?
答案:可以通过以下几种方式创建对象的原型链:
- 使用构造函数和
new
关键字。 - 使用
Object.create()
方法。 - 使用
class
关键字(ES6新引入的语法糖,背后仍然使用原型链)。
题三:描述一下__proto__和prototype之间的区别?
答案:__proto__
是每个JavaScript对象(除了null
原型对象)都内置的一个属性,指向创建该对象的构造函数的原型。这个属性是用来构建原型链的。 prototype
是函数对象特有的属性,只有函数对象才有prototype
属性。当函数对象作为构造函数用于创建实例时,被创建的对象的__proto__
属性将指向这个构造函数的prototype
属性指向的对象。
题四:何时使用hasOwnProperty方法?
答案:hasOwnProperty
是Object.prototype
的一个方法,用于检查一个属性是否为对象自身的属性,而不是继承自原型链。这在遍历一个对象的属性时特别重要,以避免执行原型链上继承的属性。
题五:解释以下代码中的原型链结构。
function A() {}
function B() {}
A.prototype = new B();
const a = new A();
答案:上述代码创建的原型链结构如下:
a
的__proto__
指向A.prototype
。A.prototype
是B
的一个实例,因此A.prototype.__proto__
指向B.prototype
。B.prototype
的__proto__
指向Object.prototype
。
题六:通过构造函数创建两个对象时,他们的原型是否相同?
答案:是的,如果两个对象是通过同一个构造函数创建的,则它们的原型对象是相同的。这是因为构造函数有一个 prototype
属性,所有由这个构造函数创建的对象实例都会共享这个原型对象。例如:
function Person() {}
var person1 = new Person();
var person2 = new Person();
console.log(person1.__proto__ === person2.__proto__); // true
在这里 person1
和 person2
同时共享 Person.prototype
。
题七:解释以下代码的输出,并解释原因。
function Animal() {}
function Dog() {}
Dog.prototype = new Animal();
var myDog = new Dog();
console.log(myDog instanceof Animal);
答案:输出将是 true
。因为 Dog.prototype
被设置为 Animal
的一个实例,所有由 Dog
构造的对象就自动拥有 Animal
在其原型链上。 instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个对象的原型链中。
题八:解释如何用原型链来实现私有方法?
答案:JavaScript 没有像其他面向对象语言那样的私有方法,但可以通过闭包和作用域链来模拟私有方法。
function MyClass() {
this.publicMethod = function() {
console.log('This is public');
privateMethod();
};
const privateMethod = function() {
console.log('This is private');
}
}
const instance = new MyClass();
instance.publicMethod(); // 输出: This is public, This is private
instance.privateMethod(); // TypeError: instance.privateMethod is not a function