前言
本文对 构造函数、原型、原型链 进行系统的介绍。经常会被问道:symbol是不是构造函数?constructor属性是否只读?prototype、[[Prototype]]和__proto__的区别?什么是原型链?等等问题。看完这篇文章,你就有答案了。
构造函数
什么是构造函数?
构造函数本身就是一个函数,与普通函数的区别在于,
用 new 创建实例的函数就是构造函数
,直接调用的就是普通函数。为了规范,构造函数的首字母一般大写。
我们用一个事例来说说,如何用构造函数来创建一个对象。分两步:
1、定义一个构造函数(可包含属性和方法)
2、通过new
调用构造函数创建实例
// 定义构造函数
function Person(name){
// 属性
this.name = name;
// 方法
this.getAge = function() {
return 18;
}
}
// 通过 new 调用函数创建实例对象
var P = new Person('谷底飞龙')
console.log(`my name is ${P.name}, my age is ${P.getAge()}`)
通过上面的例子,我们会发现通过new创建的对象可以访问构造函数内部this指向的属性和方法。
new 调用构造函数发生了什么?
调用new Person('谷底飞龙')
的执行过程,经历4个阶段:
var obj ={};
obj.__proto__ = Person.prototype;
Person.call(obj);
return obj;
- 1、创建新的空对象
obj
- 2、将构造函数Person的原型
prototype
赋值给创建的新对象obj的__proto__
,这是最关键的一步,具体细节将在下文描述。 - 3、通过
call
将新对象obj
与构造函数内部的this进行硬绑定(可参考call和apply的原理及区别),因此,新对象obj能访问到函数内部this的属性和方法。 - 4、返回一个对象(默认返回this)
obj;
Symbol 是不是构造函数?
ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。判断是否是构造函数,直接用new来调用执行试试
new Symbol();
执行后,会发现报错Uncaught TypeError: Symbol is not a constructor
。因此,Symbol
不支持使用new调用,所以不是构造函数,属于基本数据类型。
我们直接使用Symbol()
试试,会打印出Symbol()
var sym = Symbol();
console.log(sym.constructor); // ƒ Symbol() { [native code] }
console.log(sym.constructor === Symbol.prototype.constructor); // true
因此,Symbol虽然是基本数据类型,但是可以通过Symbol()
来生成实例,且我们会发现实例sym
有constructor
属性值,值为ƒ Symbol() { [native code] }
。这里的constructor
属性值哪里来的呢?其实是Symbol原型Symbol.prototype.constructor
上的,默认是Symbol()
函数。
constructor 属性是否只读?
这个得分情况,对于引用类型来说
constructor
属性值是可以修改的,但是对于基本类型来说是只读的。
- 1、引用类型的
constructor
属性是可以修改的 比如在原型链继承中,子类原型 prototype 的constructor
会被重写
// 父类
function Super() {
this.name = ['谷底飞龙']
}
// 子类
function Sub() {}
// 将 Super 的对象赋值给 Sub 的原型 prototype
Sub.prototype = new Super();
// 创建子类的两个实列
const instance = new Sub();
// 子类原型的 constructor 指向父类 Super
console.log(Sub.prototype.constructor === Super);
- 2、基本数据类型
number
、string
、bool
、Symbol
等有constructor属性的,constructor
属性是只读的。运行下面代码
let a = 1;
console.log(a.constructor);//ƒ Number() { [native code] }
console.log("谷底飞龙".constructor);//ƒ String() { [native code] }
console.log(true.constructor);//ƒ Boolean() { [native code] }
console.log(Symbol().constructor);//ƒ Symbol() { [native code] }
- 3、基本数据类型
null
、undefined
是没有constructor属性的
console.log(null.constructor);//Uncaught TypeError: Cannot read property 'constructor' of null
console.log(undefined.constructor);//Uncaught TypeError: Cannot read property 'constructor' of undefined
原型
什么是原型 prototype?
JavaScript 是基于原型的语言。
JavaScript 中所有对象都是 Object 的实例,并继承自
Object.prototype
的属性和方法。每个 JavaScript 对象都拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性
,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。
因此,给已存在的构造器添加属性和方法,需要通过原型来添加。我们先来试试不通过原型来添加属性,比如给下面的构造器 Person
增加新属性 weight
// 定义构造函数
function Person(name){
// 属性
this.name = name;
// 方法
this.getAge = function() {
return 18;
}
}
// 通过new调用函数创建实例对象
var P = new Person('谷底飞龙');
// 给 Person 增加新属性 weight
Person.weight = 65;
console.log(`my weight is ${P.weight}`)
添加属性失败,会打印出my weight is undefined
。如果要给构造器添加属性和方法,可以通过构造函数的原型 prototype
来添加,如下:
// 定义构造函数
function Person(name){
// 属性
this.name = name;
// 方法
this.getAge = function() {
return 18;
}
}
// 通过 new 调用函数创建实例对象
var P = new Person('谷底飞龙');
// 给 Person 增加新属性 weight 和方法 getHeight
Person.prototype.weight = 65;
Person.prototype.getHeight = function(){
return 165;
};
console.log(`my weight is ${P.weight}, my height is ${P.getHeight()}`)
通过构造函数的原型可成功添加属性weight
和方法 getHeight()
, 打印出my weight is 65, my height is 165
prototype、[[Prototype]]和__proto__的区别?
1、原型
prototype
是构造函数的属性,__proto__
是 new 生成的对象的属性,[[Prototype]]
是对象的内部属性。2、构造函数的原型
prototype
和其对象的__proto__
指向同一个对象3、
[[Prototype]]
指向它的构造函数的原型prototype
,外部无法直接访问,可以通过__proto__
来访问内部属性[[Prototype]]
,
从前面讲到的new调用构造函数的执行过程的第二步:obj.__proto__ = Person.prototype
,我们可以看出
- 1、原型
prototype
是构造函数的属性,__proto__
是 new 生成的对象的属性 - 2、构造函数的原型
prototype
和其对象的__proto__
是赋值关系,因此指向同一个对象。如下面的例子,会打印出true
// 定义构造函数
function Person(name){
// 属性
this.name = name;
// 方法
this.getAge = function() {
return 18;
}
}
// 通过new调用函数创建实例对象
var P = new Person('谷底飞龙')
// 对象的 __proto__ 和构造函数的原型 prototype 指向同一个对象
console.log(P.__proto__ === Person.prototype)
-
3、
[[Prototype]]
是对象的内部属性,指向它的构造函数的原型prototype
,外部无法直接访问,可以通过__proto__
来访问内部属性[[Prototype]]
,值得注意的是,__proto__
属性并非ECMAScript标准推荐使用的属性,并且是作为弃用的属性。 -
4、内部属性
[[Prototype]]
无法直接在代码中使用,要用函数Object.getPrototypeOf
来获取它的值,用函数Object.setPrototypeOf
来改变它的值。想了解的的更详细,可以看看这篇文章 Javascript:内部属性[[Prototype]]
原型链
什么是原型链?
每个对象拥有一个原型对象
__proto__
,__proto__
指向上一个原型prototype
,并继承其属性和方法,同时原型对象也有可能有原型,这样一层一层,最终指向null
,这就是原型链
。
我们来看个例子
function Person(name){
this.name = name;
}
var P = new Person('谷底飞龙')
// 打印对象
console.log(P)
打印出对象P
,如下:
我们可以看到,对象
P
有一个原型对象__proto__
,P的原型对象__proto__
有两个属性constructor
和自己的原型对象__proto__
,依次下去,最终指向null
。这个案例中的原型链关系:P.__proto__ => P.__proto__.__proto__ => null
instanceof 原理及实现
-
instanceof 原理就是一层一层查找
__proto__
,如果和constructor.prototype
相等则返回 true,如果一直没有查找成功则返回 false。 -
手动实现
instanceof
function instance_of (source, target) {
// 基本数据类型以及 null 直接返回 false
if (!['function', 'object'].includes(typeof source) || source === null) return false
// getProtypeOf 是 Object 对象自带的一个方法,能够拿到参数的原型对象 __proto__
let proto = Object.getPrototypeOf(source)
while (true) {
// 查找到尽头,还没找到
if (proto == null) return false
// 找到相同的原型对象
if (proto == target.prototype) return true
proto = Object.getPrototypeOf(proto)
}
}
原型链继承
详见 原型链继承,系统总结了 JavaScript 的各种继承方式。