构造函数、原型和原型链

268 阅读4分钟

构造函数

什么是构造函数

构造函数与普通函数的唯一区别就是调用方式不同。构造函数也是函数,并没有把某个函数定位为构造函数的特殊语法,任何函数只要使用new操作符调用就是构造函数, 直接调用的就是普通函数。按照惯例,构造函数的首字母一般要大写。

function Person(name) {
    this.name = name;
}

const p = new Person('doudou');

这个过程都发生了什么呢?

  1. 首先,在内存中创建了一个空对象
  2. 这个新对象内部的[[Prototype]]属性被赋值为构造函数的prototype属性
  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性和方法)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

通过new方式创建的实例对象都有一个constructor属性,这个属性指向实例对象的构造函数。

p.constructor === Person; // true
p.constructor === Object; // false

那是不是意味这普通函数创建的实例没有constructor属性呢?不一定

// 普通函数
function parent2(age) {
    this.age = age;
}
var p2 = parent2(50);
// undefined

// 普通函数
function parent3(age) {
    return {
        age: age
    }
}
var p3 = parent3(50);
p3.constructor === Object; // true

constructor是只读的吗?

  • 对于引用类型来说contructor属性值使可以修改的
  • 对于基本类型来说是只读的

引用类型contructor属性值使可以修改比较好理解,比如原型链继承方案中,就需要对constructor属性重新赋值进行修正。

// 原型链继承
function SuperType () {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}

function SubType () {
  this.subProperty = false
}

// 继承SuperType
SubType.prototype = new SuperType();
// 修正constructor指向
SubType.prototype.constructor = SubType;

SubType.prototype.getSubValue = function () {
  return this.subProperty
}
const instance = new SubType();
console.log(instance.constructor === SubType); // true
console.log(instance.getSuperValue()) // true

对于基本类型来说是只读的,比如1、"doudou"、true、Symbol,当然null和undefined是没有constructor属性的。

function Type() { };
var	types = [1, "doudou", true, Symbol(123)];

for(var i = 0; i < types.length; i++) {
	types[i].constructor = Type;
	types[i] = [ types[i].constructor, types[i] instanceof Type, types[i].toString() ];
};

console.log( types.join("\n") );
// function Number() { [native code] }, false, 1
// function String() { [native code] }, false, doudou
// function Boolean() { [native code] }, false, true
// function Symbol() { [native code] }, false, Symbol(123)

为什么呢?因为创建他们的是只读的原生构造函数(native constructors)。

模拟实现new

可查看手写系列——手写一个new的实现

原型

五个规则

先来了解五个规则:

  1. 所有引用类型,都具有对象特性,即可自由扩展属性(可动态的添加属性和方法)
  2. 所有引用类型,都有一个__proto__属性(隐式原型),属性值是一个普通对象
  3. 所有函数,都有一个prototype属性,属性值也是一个普通对象
  4. 所有引用类型,__proto__的属性值指向它的构造函数的prototype属性值
  5. 当你试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型__proto__(也就是它的构造函数的显式原型prototype)中寻找

prototype

JavaScript是一种基于原型的语言,这个和其他基于类的语言不一样。

  • 所有的函数,都有一个prototype属性(显式原型),属性值是一个普通对象。 image.png 从上面图中可以发现,Person对象有一个原型对象Person.prototype, 它的属性值是一个对象,包含两个属性,分别是constructor和[[Prototype]]。

构造函数Person有一个指向原型的指针,原型Person.prototype有一个指向构造函数的指针Person.prototype.constructor, 如下图所示:

image.png

__proto__

  • 所有引用类型(数组、对象、函数),都有一个__proto__属性(隐式原型),属性值也是一个普通对象。
  • 所有引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值。
function Person() {}
var p = new Person();
p.__proto__ === Person.prototype // true

这里用p.__proto__获取对象的原型,__proto__是每个实例上都有的属性,property是构造函数的属性,这两个并不一样,但p.__proto__和Person.prototype指向同一个对象。

构造函数Person、Person.prototype和实例p的关系如下图:

image.png

注意

proto 属性在 ES6 时才被标准化,以确保Web浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()。

如果要读取或修改对象的 [[Prototype]]属性,建议使用如下方案,但是此时设置对象的[[Prototype]]依旧是一个缓慢的操作,如果性能是一个问题,就要避免这种操作。

// 获取
Object.getPrototypeOf()
Reflect.getPrototypeOf()

// 修改
Object.setPrototypeOf()
Reflect.setPrototypeOf()

如果要创建一个新对象,同时继承另一个对象的 [[Prototype]] ,推荐使用 Object.create()。

function Person() {
    age: 50
};
const p = new Person();
const child = Object.create(p); // child 是一个新的空对象,有一个指向对象 p 的指针 __proto__

Object.create()的基本实现思路:

function create(obj) {
    const F = function() {}
    F.prototype = obj
    return new F()
}

原型链

当代码读取一个对象的某个属性时,首先会从对象实例本身开始寻找。如果在实例中找到了该属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找该属性,如果在原型对象中找到了该属性,则返回该属性的值,否则继续往上查找。

function Parent(age) {
    this.age = age;
}

var p = new Parent(50);
p.constructor === Parent; // true

这里 p.constructor 指向 Parent,那是不是意味着 p 实例存在 constructor 属性呢?并不是。

我们打印下 p 值就知道了。

image.png

由图可以看到实例对象 p 本身没有 constructor 属性,是通过原型链向上查找 __proto__ ,最终查找到 constructor 属性,该属性指向 Parent

function Parent(age) {
    this.age = age;
}
var p = new Parent(50);

p;	// Parent {age: 50}
p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true

下图展示了原型链的运作机制:

image.png

原型和实例关系的检测方法——instanceof

instanceof运算符用于检测构造函数的prototype属性是否存在于实例对象的原型链上。

function C(){} 
function D(){} 

var o = new C();

o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上

instanceof原理就是一层一层查找__proto__,如果和构造函数的prototype相等则返回true,如果一直没有查找成功则返回false

instance.[__proto__...] === instance.constructor.prototype

模拟实现instanceof

function instance_of(L, R) { // L 表示左边表达式 R表示右边表达式
    const O = R.prototype; // 取R的显式原型
    L = Object.getPrototypeOf(L); // 取L的隐式原型 L = L.__proto__
    while(true) {
    // Object.prototype.__proto__ === null
        if(L===null) {
            return false;
        }
        if(L===O) { // 当L严格等于O时,返回true
            return true;
        }
        L = Object.getPrototypeOf(L); // // 没找到继续向上一层原型链查找 L = L.__proto__
    }
}
function Foo(name) {
  this.name = name;
}
var f = new Foo('nick')

f instanceof Foo // true
f instanceof Object // true

参考:
muyiy.cn/blog/5/5.1.…
juejin.cn/post/693449…