构造函数、原型和原型链

198 阅读5分钟

构造函数

什么是构造函数

constructor返回创建实例对象时构造函数的引用。此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。

function Parent(age) {
    this.age = age;
}
var p = new Parent(50);
p.constructor === Parent; // true
p.constructor === Object; // false

构造函数本身就是一个函数,与普通函数没有任何区别,不过为了规范一般将其首字母大写构造函数和普通函数的区别在于,使用new生成实例的函数就是构造函数,直接调用的就是普通函数。

那是不是意味着普通函数创建的实例没有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

Symbol是构造函数吗

MDN是这样介绍Symbol的

The Symbol() function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()".

Symbol是基本数据类型,但作为构造函数来说它并不完整,因为它不支持语法 new Symbol(),Chrome认为其不是构造函数,如果要生成实例直接使用Symbol()即可。(来自MDN)

new Symbol(123); // Symbol is not a constructor

Symbol(123); // Symbol(123)

虽然是基本数据类型,但Symbol(123)实例可以获取constructor属性值。

var sym = Symbol(123);
console.log(sym);
// Symbol(123)
console.log(sym.constructor);
// f Symbol(){ [native code] }

这里的constructor属性来自哪里?其实是Symbol原型上的,即Symbol.prototype.constructor返回创建实例原型的函数,默认为Symbol函数。

constructor值只读吗

对于引用类型来说constructor属性值是可以修改的,但是对于基本类型来说是只读的。

引用类型情况其值可修改这个很好理解,比如原型链继承方案中,就需要对constructor重新赋值进行修正。

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method:function() {}
};
function Bar() {}

// 设置 Bar 的 prototype属性为Foo 的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

Bar.prototype.constructor === Object;
// true

// 修正 Bar.prototypr.constructor 为 Bar 本身
Bar.prototype.constructor = Bar

var test = new Bar() // 创建 Bar 的一个新实例
console.log(test);

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

function Type() { };
var types = [1,"liming",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,liming
// function Boolean() {[native code]},flase,true
// function Symbol() {[native code]},false,Symbol(123)

为什么呢?因为创建他们的是只读的原生构造函数(native constructors),这个例子也说明了依赖一个对象的constructor属性并不安全。

模拟实现new

new的实现,实现代码如下。

function create() {
    // 1.创建一个空的对象
    var obj = new Object();
    // 2.获得构造函数,同时删除arguments中的第一个参数
    Con = [].shift.call(arguments);
    // 3.链接到原型,obj可以访问构造函数原型中的属性
    Object.setPrototypeOf(obj,Con.prototype);
    // 4.绑定this实现继承,obj可以访问到构造函数中的属性
    var ret = Con.apply(obj,arguments);
    // 5.优先返回构造函数返回的对象
    return ret instanceof Object ? ret:obj;
};

原型

prototype

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

每个对象拥有一个原型对象,对象以其原型为模版,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的prototype属性上,而非对象实例本身。

image.png

从上面图中可以发现,Parent 对象有一个原型对象Parent.prototype,其上有两个属性,分别是constructor 和 proto,其中 proto 已被弃用。

构造函数Parent 有一个指向原型的指针,原型Parent.prototype有一个构造函数的指针Parent.prorotype.constructor,如上图所示,其实就是一个循环引用。

image.png

proto

上图可以看到Parent原型(Parent.prototype)上有__proto__属性,这是一个访问器属性(即getter函数和setter函数),通过它可以访问到对象的内部[[Prototype]](一个对象或null)。

proto 发音 dunder proto,最先被Firefox使用,后来在ES6被列为JavaScript的标准内建属性。

[[Prototype]] 是对象的一个内部属性,外部代码无法直接访问。

遵循ECMAScript标准,someObject.[[Prototype]]符号用于指向someObject的原型。

image.png

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

function Parent() {};
var p = new Parent();
p.__proto__ === Parent.prototype;
// true

所以构造函数Parent、Parent.prototype 和 p的关系如下图。

image.png

注意点

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

通过改变一个对象的[[Prototype]]属性来改变和继承属性会对性能造成非常严重的影响,并且性能消耗的时间也不是简单的花费在obj.proto = ...语句上,它还会影响到所有继承自该[[Prototype]]的对象,如果你关心性能,你就不应该修改一个对象的[[Prototype]]。

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

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

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

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

function Parent() {
    age:50
};
var p = new Parent();
var child = Object.create(p);

这里child是一个新的空对象,有一个指向对象p的指针__proto__ 。

优化实现new

正如上面介绍的不建议使用__proto__,所以我们使用Object.create()来模拟实现,优化后的代码如下。

function create() {
    // 1.获得构造函数,同时删除arguments中第一个参数
    Con = [].shift.call(arguments);
    // 2.创建一个空的对象并链接到原型,obj可以访问构造函数原型中的属性
    var obj = Object.create(Con.prototype);
    // 3. 绑定this实现继承,obj可以访问到构造函数中的属性
    var ret = Con.apply(obj,arguments);
    // 4.优先返回构造函数返回的对象
    return ret instanceof Object ? ret:obj;
}

原型链

每个对象拥有一个原型对象,通过__prototype__指针指向上一个原型,并从中继承方法和属性,同时原型对象也有可能拥有原型,这样一层一层,最终指向null。这种关系被称为原型链,通过原型链一个对象会拥有定义在其他对象中的属性和方法。

看下面一个例子

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

小结

  • Symbol作为构造函数来说并不完整,因为不支持语法new Symbol(),但其原型上拥有constructor属性,即Symbol.prototype.constructor。
  • 引用类型constructor属性值是可以修改的,但是对于基本类型来说是只读的,当然null和undefined 没有 constructor属性。
  • proto 是每个实例上都有的属性,prototype是构造函数的属性,在实例上并不存在,所以这两个并不一样,但p.proto 和 Parent.prototype指向同一个对象。
  • proto 属性在ES6时被标准化,但因为性能问题并不推荐使用,推荐使用Object.getPrototypeOf()。
  • 每个对象拥有一个原型对象,通过__proto__指针指向上一个原型,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null,这就是原型链。