前言
-
说一说你对JavaScript中原型与原型链的理解?
-
对一个构造函数实例化后,它的原型链指向什么?
原型与原型链介绍
在Brendan Eich设计JavaScript时,借鉴了Self和Smalltalk这两门基于原型的语言。
之所以选择基于原型的对象系统,是因为Brendan Eich一开始就没有打算在JavaScript中加入类的概念,因为JavaScript的设计初衷就是为非专业的开发人员(例如网页设计者)提供一个方便的工具。由于大部分网页设计者都没有任何的编程背景,所以在设计JavaScript时也是尽可能使其简单、易学。
这因为如此,JavaScript中的原型以及原型链成为了这门语言最大的一个特点,在面试的时候,面试官也经常会绕原型和原型链展开提问。
JavaScript是一门基于原型的语言,对象的产生是通过原型对象而来的。
ES5中提供了Object.create方法,可以用来克隆对象。
const obj = { a: 1 };
const cloneObj = Object.create(obj);
console.log(cloneObj); // {}
console.log(cloneObj.a); // 1
console.log(cloneObj.__proto__ === obj); // true
在上面的示例中,我们通过Object.create方法来对obj对象进行克隆,克隆出来了一个名为cloneObj的对象,所以obj对象就是cloneObj这个对象的原型对象。
obj对象上面的属性和方法,cloneObj这个对象上面都有。
通过__proto__属性,我们可以访问到一个对象的原型对象。
从上面的代码可以看出,当我们打印cloneObj.__proto__ === obj时为true,因为对于cloneObj这个对象而言它的原型对象就是obj这个对象。
我们在使用Object.create方法来克隆对象的时候,还可以传入第2个参数,第2个参数是个JSON对象,该对象可以书写新对象的新属性以及属性特性。
通过这种方式,基于对象创建的新对象,可以继承祖辈对象的属性和方法,这其实就是个继承的关系,如下示例:
const obj = { a: 1 };
const cloneObj = Object.create(obj, {
b: {
value: 2,
writable: true,
configurable: true,
enumerable: true,
},
});
const cloneObj2 = Object.create(cloneObj, {
c: {
value: 3,
writable: true,
configurable: true,
enumerable: true,
},
});
console.log(cloneObj2); // { c: 3 }
console.log(cloneObj2.a); // 1
console.log(cloneObj2.b); // 2
console.log(cloneObj2.c); // 3
console.log(cloneObj2.__proto__ === cloneObj); // true
console.log(cloneObj2.__proto__.__proto__ === obj); // true
该例中,cloneObj这个对象是从obj这个对象克隆而来的,而cloneObj2这个对象又是从cloneObj这个对象克隆而来,以此形成了一条原型链。无论是obj对象,还是cloneObj对象上面的属性和方法,cloneObj2这个对象都能继承到。
这就是JavaScript中最原始的创建对象的方式,一个对象是通过克隆另外一个对象所得到的。被克隆的对象是新对象的原型对象。
但是,随着JavaScript语言的发展,这样创建对象的方式还是太过于麻烦了。开发者还是期望JavaScript能够像Java、C#等标准面向对象语言一样,通过类来批量的生成对象。于是出现了通过构造函数来模拟类的形式。
function Person (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.getName = function () {
return this.name;
};
const p1 = new Person("张三", 18);
const p2 = new Person("李四", 20);
console.log(p1); // { name: '张三', age: 18 }
console.log(p1.getName()); // 张三
console.log(p2); // { name: '李四', age: 20 }
在上面的例子中,我们书写了一个Person的函数,我们称之为构造函数,为了区分普通函数和构造函数,一般构造函数的函数名首字母会大写。
区别于普通函数的直接调用,构造函数一般通过配合 new 关键字一起使用,每当我们 new 一次,就会生成一个新的对象,而在构造函数中的 this 就指向这个新生成的对象。
在上面的例子中,我们 new 了两次,所以生成了两个对象,我们把这两个对象分别存储到 p1 和 p2 这两个变量里面。
有一个非常有意思的现象,就是我们在书写Person构造函数的实例方法的时候,并没有将这个方法书写在构造函数里面,而是写在了Person.prototype上面,那么这个Person.prototype是啥呢?
这个Person.prototype实际上就是Person实例对象的原型对象。要高清楚这个,看下面的图:
通过上图,我们可以得出以下的结论:
- JavaScript 中每个对象都有一个原型对象。可以通过__proto__属性来访问到对象的原型对象。
- 构造函数的 prototype 属性指向一个对象,这个对象是该构造函数实例化出来的对象的原型对象。
- 原型对象的 constructor 属性也指向其构造函数。
- 实例对象的 constructor 属性是从它的原型对象上面访问到。
实践才是检验真理的唯一标准。接下来我们在代码中来验证一下:
function Person (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.getName = function () {
return this.name;
};
const p1 = new Person("张三", 18);
console.log(p1.__proto__ === Person.prototype); // true
console.log(p1.__proto__.constructor === Person); // true
在上面的代码中,p1 是从 Person 这个构造函数中实例化出来的对象,我们通过__proto__来访问到 p1 的原型对象,而这个原型对象和 Person.prototype 是等价的。另外,我们也发现 p1 和它原型对象的 constructor 属性都指向 Person 这个构造函数。
接下来我们还可以来验证内置的构造函数是不是也是这样的关系,如下:
function Person (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.getName = function () {
return this.name;
};
const p1 = new Person("张三", 18);
// 数组的三角关系
var arr = [];
console.log(arr.__proto__ === Array.prototype); // true
// 其实所有的构造函数的原型对象都相同
console.log(Person.__proto__ === Array.__proto__); // true
console.log(Person.__proto__ === Date.__proto__); // true
console.log(Person.__proto__ === Number.__proto__); // true
console.log(Person.__proto__ === Function.__proto__); // true
console.log(Person.__proto__ === Object.__proto__); // true
console.log(Person.__proto__); // {}
通过上面的代码,我们发现所有的构造函数,无论是自定义的还是内置的,它们的原型对象都是同一个对象。
如果你能够把上面的三角关系理清楚,恭喜你,你已经把整个原型和原型链的知识掌握一大部分。
如果你还想继续往下深究,那么上面的图可以扩展成这样:
在 JavaScript 中,每一个对象,都有一个原型对象。而原型对象上面也有一个自己的原型对象,一层一层向上找,最终会到达 null。
我们可以在上面代码的基础上,继续进行验证,如下:
function Person (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.getName = function () {
return this.name;
};
const p1 = new Person("张三", 18);
console.log(p1.__proto__.__proto__); // [Object: null prototype] {}
console.log(p1.__proto__.__proto__.__proto__); // null
console.log(p1.__proto__.__proto__ === Object.prototype); // true
可以看到,在上面的代码中,我们顺着原型链一层一层往上找,最终到达了 null。
但是目前来看我们这个图还是不完整,既然构造函数的原型对象也是对象,那么必然该对象也有自己的原型,所以完整的图其实如下:
下面可以简单验证一下,如下:
function Person (name, age) {
this.name = name;
this.age = age;
};
console.log(Person.__proto__.__proto__.__proto__); // null
console.log(Person.__proto__.constructor.__proto__ === Person.__proto__); // true
console.log(Person.__proto__.__proto__.constructor.__proto__ === Person.__proto__); // true
总结
- 说一说你对JavaScript中原型与原型链的理解?
- 每个对象都有一个__proto__属性,该属性指向自己的原型对象
- 每个构造函数都有一个 prototype 属性,该属性指向实例对象的原型对象
- 原型对象里的 constructor 指向构造函数本身
每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
- 对一个构造函数实例化后,它的原型链指向什么?
指向该构造函数实例化出来对象的原型对象。
对于构造函数来讲,可以通过 prototype 访问到该对象。
对于实例对象来讲,可以通过隐式属性__proto__ 来访问到。