欢迎关注微信公众号:前端阅读室
构造函数
在 JavaScript 中,我们可以使用构造函数创建对象。
function Student() {}
var student = new Student();
student.name = "tom";
console.log(student);
这里 Student 就是构造函数,我们使用 new 操作符创建了一个 student 对象。
prototype
每个函数都有 prototype 属性,它的值也是一个对象。我们给它设置属性,看看会发生什么?
function Student() {}
Student.prototype.work = "study";
var student1 = new Student();
var student2 = new Student();
console.log(student1.work);
console.log(student2.work);
你会发现一件神奇的事,这时打印出来的 student1 和 student2 的 work 属性都是 study!那么函数的 prototype 属性到底是什么呢?
一句话:函数的 prototype 属性指向一个对象,这个对象是调用该(构造)函数创建出的实例的原型。
那么什么是原型呢?所有 JavaScript 对象(null 除外)在创建的时候都有一个与之关联的对象,这个对象就是原型,对象会继承原型上的属性或方法。
我们用一张图来表示构造函数和实例原型之间的关系:
__proto__
我们前面知道了构造函数(Student)和实例原型(student)可以通过构造函数的 prototype 属性(Student.prototype)连接起来,那么实例和实例原型又是怎么联系起来的呢?
答案就是:__proto__。
JavaScript 对象(除了 null)都有一个__proto__属性,这个属性指向的就是该对象的原型。
我们来验证下:
function Student() {}
var student = new Student();
console.log(student.__proto__ === Student.prototype);
现在关系图就变成这样了:
既然实例和构造函数都可以指向原型,那么原型是否有属性指向实例或构造函数呢?
constructor
原型没有指向实例的属性,因为一个构造函数可以生成多个实例,要实现这种指向关系显示是不现实的。不过原型倒是有指向构造函数的属性,它就是 constructor。
我们来验证下:
function Student() {}
console.log(Student === Student.prototype.constructor);
此时关系图变为:
至此,我们知道了构造函数、实例和实例原型之间的关系。接下来我们来具体介绍下实例和原型的关系。
实例和原型
我们先来看一段代码,大家猜猜它会打印什么?
function Student() {}
Student.prototype.work = "study";
var student = new Student();
student.work = "play";
console.log(student.work);
delete student.work;
console.log(student.work);
第一个打印的是 play,第一个打印的是 study,为什么会这样呢?
答案是:当读取实例对象属性时,会优先查找对象本身的属性,如果查找不到,则会去对象的原型上查找,如果还查找不到,则会查找原型的原型,一直查找到最顶层的原型为止。
所以当 student 对象本身有 work 属性时,会打印 play。当对象本身的 work 属性被删除时,会去原型上查找,打印出 study。
原型的原型
我们前面提到了原型的原型,那么它是什么呢?
我们打印一下就知道了,我们前面介绍了对象的__proto__属性指向它的原型,原型也是对象,它的__proto__属性就指向了原型的原型。一直打印,就能查找到最顶层的原型。
function Student() {}
Student.prototype.work = "study";
var student = new Student();
student.work = "play";
console.log(student.__proto__);
console.log(student.__proto__.__proto__);
console.log(student.__proto__.__proto__.__proto__);
console.log(student.__proto__.__proto__.__proto__.__proto__);
我们看到 student.__proto__ 是 student 的原型,student.__proto__.__proto__我们没介绍过,它是 Object.prototype(Object 构造函数的 prototype 属性),student.__proto__.__proto__.__proto__是 null(null 表示"没有对象",即 Object.prototype 没有原型)。null 上面就没有原型了,所以打印会抛错。
一般来说,对象的原型其实都是通过 Object 构造函数生成的,所以对象的顶层原型一般都是 Object.prototype。
当然也有特例,比如:
var obj = Object.create(null);
console.log(obj.__proto__);
打印的结果是 undefined,它并没有原型。
我们再更新最后一张关系图:
原型链
前面其实我们已经接触过原型链了,还是我们来看下与前面相似的这段代码:
function Student() {}
var student = new Student();
console.log(student.work);
console.log(student.__proto__); // Student.prototype
console.log(student.__proto__.__proto__); // Object.prototype
console.log(student.__proto__.__proto__.__proto__); // null
如果我们要打印 student 的 work 属性,它会先查找 student 对象本身,然后再查找 Student.prototype,最后再查找 Object.prototype,发现 Object.prototype 上也没有 work 属性,就停止查找,打印出 undefined。
这种由相互关联的原型组成的链式结构就是原型链。
补充
constructor
function Student() {}
var student = new Student();
console.log(student.constructor === Student);
console.log(student.constructor === Student.prototype.constructor);
构造函数的引用(constructor 属性),并不在实例对象本身上,而是在实例对象的原型上。
__proto__
绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上它是来自于 Object.prototype。所以与其说__proto__是一个属性,不如说它是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。
真的是继承吗?
我们前面提到过对象会继承原型上的属性或方法。实际上它并不是真的继承,因为继承就意味着复制操作,然而 JavaScript 只是在对象和原型之间建立了关联关系,并不是复制原型的属性和方法。所以相比于继承,其实用委托这个说法更贴切一些。
欢迎关注微信公众号:前端阅读室