「JavaScript进阶」一文吃透构造函数、原型、原型链

678 阅读6分钟

前言

本文对 构造函数、原型、原型链 进行系统的介绍。经常会被问道: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()来生成实例,且我们会发现实例symconstructor属性值,值为ƒ 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、基本数据类型numberstringboolSymbol等有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、基本数据类型nullundefined是没有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,如下: image.png 我们可以看到,对象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 的各种继承方式。

更多精彩