网站搜索

继承 - 在 Javascript 中,'Object.create' 和 'new' 的区别


我想我已经在脑海中理清了区别,但我只是想确认一下。

在道格拉斯·克罗克福德的页面《JavaScript中的原型继承》中,他说

在原型系统中,对象从对象继承。然而,JavaScript 缺少执行该操作的运算符。相反,它有一个 new 运算符,使得 new f() 产生一个继承自 f.prototype 的新对象。

我并不真正理解他在那句话中想表达的意思,所以我做了一些测试。在我看来,关键的区别是,如果我在一个纯原型系统中基于另一个对象创建一个对象,那么所有父级的成员应该在新对象的原型上,而不是在新对象本身上。

这是测试:

var Person = function(name, age) {
        this.name = name;
        this.age = age;
}
Person.prototype.toString = function(){return this.name + ', ' + this.age};

// The old way...
var jim = new Person("Jim",13);
for (n in jim) {
    if (jim.hasOwnProperty(n)) {
        console.log(n);
     }
}
// This will output 'name' and 'age'.

// The pure way...
var tim = Object.create(new Person("Tim",14));
for (n in tim) {
    if (tim.hasOwnProperty(n)) {
        console.log(n);
     }
}
// This will output nothing because all the members belong to the prototype.
// If I remove the hasOwnProperty check then 'name' and 'age' will be output.

我的理解是否正确,即只有在测试对象本身的成员时,这种差异才会显现?

答案#1

你的假设是正确的,但还有另一种模式 Douglas 没有多谈——原型也可以用于属性。你的 Person 类本可以这样写:

var Person = function(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.name = null; //default value if you don't init in ctor
Person.prototype.age = null;
Person.prototype.gender = "male";
Person.prototype.toString = function(){return this.name + ', ' + this.age;};

在这种情况下,对该类的实例的属性进行迭代,就像你在示例中所做的那样,对 'gender' 属性不会产生任何输出。

编辑 1:在构造函数中分配 name 和 age 确实会使这些属性通过 hasOwnProperty 可见(感谢 @matt 提醒我这一点)。未分配的 gender 属性在有人在实例上设置之前将不可见。

编辑2:为了进一步补充这一点,我提出一种替代的继承模式——一种我个人在非常大型项目中使用过的模式:

var inherits = function(childCtor, parentCtor) {
  function tempCtor() {};
  tempCtor.prototype = parentCtor.prototype;
  childCtor.superclass = parentCtor.prototype; 
  childCtor.prototype = new tempCtor();
  childCtor.prototype.constructor = childCtor;
};

var Person = function(name){
    this.name = name;
}
Person.prototype.name = "";
Person.prototype.toString = function(){
    return "My name is " + this.name;
}

var OldPerson = function(name, age){
    OldPerson.superclass.constructor.call(this);
    this.age = age
};
inherits(OldPerson, Person);
OldPerson.prototype.age = 0;
OldPerson.prototype.toString = function(){
    var oldString =  OldPerson.superclass.toString.call(this);
    return oldString + " and my age is " + this.age;
}

这是一个相当常见的模式,但有一个小小的变化——父类通过“superclass”属性附加到子类,使你能够访问被子类重写的方法/属性。从技术上讲,你可以用 Person 替换 OldPerson.superclass,但是那并不理想。如果你将来把 OldPerson 改为继承除了 Person 之外的其他类,你也必须更新对 Person 的所有引用。

编辑3:为了完整说明,这里有一个“继承”函数的版本,它利用了Object.create,并且功能完全和我之前描述的一样:

var inherits = function(childCtor, parentCtor) {
    childCtor.prototype = Object.create(parentCtor.prototype);
    childCtor.superclass = parentCtor.prototype; 
};

答案#2

编辑:这个答案最初是对 @jordancpaul 的回答的回复,他后来已经更正了。我会保留我答案中有助于解释原型属性和实例属性之间重要区别的部分:

在某些情况下,属性是在所有实例之间共享的,因此每当你在原型上声明属性时都需要非常小心。考虑以下示例:

Person.prototype.favoriteColors = [];  //Do not do this!

现在,如果你使用 Object.create 或 new 创建一个新的 Person 实例,它的工作方式可能并不像你预期的那样…

var jim = new Person("Jim",13);
jim.favoriteColors.push('red');
var tim = new Person("Tim",14);
tim.favoriteColors.push('blue');

console.log(tim.favoriteColors); //outputs an array containing red AND blue!

这并不意味着你永远不能在原型上声明属性,但如果你这样做,你以及所有处理你代码的开发者都需要意识到这个陷阱。在这种情况下,如果你出于某种原因更喜欢在原型上声明属性,你可以这样做:

Person.prototype.favoriteColors = null

并在构造函数中将其初始化为空数组:

var Person = function(name, age) {
    ...
    this.favoriteColors = [];
}

使用此方法的一般规则是,对于简单字面量属性(字符串、数字、布尔值),可以直接在原型上设置默认值,但任何继承自 Object 的属性(包括数组和日期)应设置为 null,然后在构造函数中初始化。

更安全的方法是仅在原型上声明方法,并始终在构造函数中声明属性。

无论如何,问题是关于 Object.create 的……

传递给 Object.create 的第一个参数会被设置为新实例的原型。更好的用法是:

var person = {
    initialize: function(name, age) {
        this.name = name;
        this.age = age;
        return this;
    },

    toString: function() {
        return this.name + ', ' + this.age;
    }
};

var tim = Object.create(person).initialize("Tim",14);

现在输出将与您的第一个示例相同。

正如你所看到的,这是一种与 JavaScript 中更经典的面向对象风格不同的哲学方法。使用 Object.create 时,重点是从现有对象创建新对象,而不是关注构造函数。初始化随后成为一个单独的步骤。

就个人而言,我对 Object.create 方法有复杂的感受;它在继承方面非常好,因为可以使用第二个参数向现有原型添加额外属性,但它也更冗长,并且使 instanceof 检查不再起作用(在这个例子中,替代方法是检查 person.isPrototypeOf(tim))。

我说 Object.create 冗长的主要原因是第二个参数,但有一些有用的库可以解决这个问题:

https://github.com/Gozala/selfish

https://github.com/Raynos/pd

(以及其他)

我希望那比让人困惑更有启发性!