JavaScript继承:理解构造函数属性

280 阅读5分钟

翻译:道奇
作者:Dmitri Pavlutin
原文:Inheritance in JavaScript: Understanding the constructor Property

JavaScript有一种有趣的继承机制:prototypal(原型链),大部分刚开始JavaScript的开发人员很难理解它,我也是。

JavaScript中的所有类型(除了nullundefined值)都有构造函数属性,它是继承的一部分。例如:

var num = 150;
num.constructor === Number // => true
var obj = {};
obj.constructor === Object // => true
var reg = /\d/g;
reg.constructor === RegExp; // => true

在这篇文章中,我们将深入学习对象的构造函数属性,它作为类的公共特性,意味着它可以用于:

  • 标识出对象属于哪个类(instanceOf的另外选择)
  • 从对象或原型(prototype)引用构造函数
  • 获取类名

1.原始类型的构造函数

JavaScript中,原始类型指的是数字、布尔、字符串、symbol(ES6新增类型),nullundefined。除了nullundefined之外的任何值都有构造函数属性,该属性指的是对应的类型函数:

  • 数字的Number(): (1).constructor === Number
  • 布尔类型的Boolean(): (true).constructor === Boolean
  • 字符串的String(): ('hello').constructor === String
  • SymbolSymbol(): Symbol().constructor === Symbol

通过将它与相应的函数进行比较,可将原始类型的构造函数属性用于确定它的类型,例如,验证值是否是数字:

if (myVariable.constructor === Number) {
   // 当myVariable是数字时,代码才执行
   myVariable += 1;
}

注意,这种方法一般不推荐,更推荐typeof的方式(见1.1),但这种方法在switch语句中很有用,可以减少if/else的数量:

// myVariable = ...
var type;
switch (myVariable.constructor) {
  case Number:
    type = 'number';
    break;
  case Boolean:
    type = 'boolean';
    break;
  case String:
    type = 'string';
    break;
  case Symbol:
    type = 'symbol';
    break;
  default:
    type = 'unknown';
    break;
}

1.1 原始值的包装对象

通过new运算符调用函数时,会创建一个原始值的包装对象,new String('str'),new Number(15)new Boolean(true)都可以创建一个包装对象,但Symbol是不会创建包装对象的,因为以new Symbol('symbol')这种方式调用会产生类型异常的错误。

包装对象允许开发人员将自定义属性和方法绑定到原始值上,因为JavaScript不允许原始值有自己的属性。

在基于构造函数判断变量的类型时,这些包装对象的存在可能会对造成理解上的混乱,因为包装对象具有与原始值相同的构造函数:

var booleanObject = new Boolean(false);
booleanObject.constructor === Boolean // => true

var booleanPrimitive = false;
booleanPrimitive.constructor === Boolean // => true

2.原型(prototype)对象的构造函数

原型(prototype)中的构造函数属性会自动设置为构造函数的引用。

function Cat(name) {
  this.name = name;
}
Cat.prototype.getName = function() {
  return this.name;
}
Cat.prototype.clone = function() {
  return new this.constructor(this.name);
}
Cat.prototype.constructor === Cat // => true

因为属性是继承自原型(prototype)的,实例对象也有构造函数。

var catInstance = new Cat('Mew');
catInstance.constructor === Cat // => true

甚至从直接量上创建的对象,也是继承自Object.prototype

var simpleObject = {
  weekDay: 'Sunday'
};
simpleObject.prototype === Object // => true

2.1 不要在子类中丢失构造函数

构造函数是原型对象中常规的不可枚举属性,当基于它创建新的对象的时候,它不会自动更新。当创建子类时,需要手动设置正确的构造函数。

下面的例子为超类Cat创建一个子类Tiger。注意初始化时Tiger.prototype仍然指向Cat构造函数。

function Tiger(name) {
   Cat.call(this, name);
}
Tiger.prototype = Object.create(Cat.prototype);
//prototype有个不正确的构造函数
Tiger.prototype.constructor === Cat   // => true
Tiger.prototype.constructor === Tiger // => false

现在如果使用Cat.prototype中定义的clone()方法克隆一个Tiger实例,会创建一个错误的Cat实例。

var tigerInstance = new Tiger('RrrMew');
var wrongTigerClone = tigerInstance.clone();
tigerInstance instanceof Tiger    // => true
// 注意wrongTigerClone是个不正确的Cat实例
wrongTigerClone instanceof Tiger  // => false
wrongTigerClone instanceof Cat    // => true

会出错的原因是Cat.prototype.clone()使用 new this.constructor()创建新的备份,但构造函数始终指向Cat函数。

为了解决这个问题,必需使用正确的构造函数:Tiger,手动更新Tiger.prototype,这样clone()方法也会被修复。

//修改Tiger原型构造函数
Tiger.prototype.constructor = Tiger;
Tiger.prototype.constructor === Tiger // => true

var tigerInstance = new Tiger('RrrMew');
var correctTigerClone = tigerInstance.clone();

//注意correctTigerClone是正确的Tiger实例
correctTigerClone instanceof Tiger  // => true
correctTigerClone instanceof Cat    // => false

查看此demo以获得完整的示例。

instanceof的另一个选择

object instanceof Class用于确定对象是否和Class有同样的prototype

这个操作符也会在原型链里进行搜索,这样做有时候会使得区分子类实例和超类实例变得很困难,例如:

var tigerInstance = new Tiger('RrrMew');

tigerInstance instanceof Cat   // => true
tigerInstance instanceof Tiger // => true

就像这个例子中看到的,不太可能准确的确认tigerInstanceCat还是Tiger,因为instanceof在两种情况下都返回true。 这就是构造函数属性的闪光点,它允许严格确定实例类。

tigerInstance.constructor === Cat   // => false
tigerInstance.constructor === Tiger // => true

// 或者使用switch
var type;
switch (tigerInstance.constructor) {
  case Cat:
    type = 'Cat';
    break;
  case Tiger:
    type = 'Tiger';
    break;
  default:
    type = 'unknown';    
}
type // => 'Tiger'

获取类名

JavaScript中的函数对象有个属性名称,它返回函数名或匿名函数的空字符串。

除了构造函数属性之外,这对于确定类名也很有用,作为Object.prototype.toString.call(objectInstance)的另外一种选择。

var reg = /\d+/;
reg.constructor.name                // => 'RegExp'
Object.prototype.toString.call(reg) // => '[object RegExp]'

var myCat = new Cat('Sweet');
myCat.constructor.name                // => 'Cat'
Object.prototype.toString.call(myCat) // => '[object Object]'

但是name返回匿名函数的空字符串(但是在ES6中,名称可以推断出来),这种方法应该谨慎使用。

总结

构造函数属性是JavaScript的继承机制的一小部分。创建类的层级结构时应采取预防措施, 但是,它提供了确定实例类型的很好的替代方法。

还可以看一下
Object.prototype.constructor
What’s up with the constructor property in JavaScript?