扩展JavaScript中的对象

199

我目前正在从Java转向Javascript,但我很难弄清楚如何按照我所希望的方式扩展对象。

我在互联网上看到过几个人使用一种叫做extend的方法来扩展对象。代码看起来像这样:

var Person = {
   name : 'Blank',
   age  : 22
}

var Robot = Person.extend({
   name : 'Robo',
   age  : 4
)}

var robot = new Robot();
alert(robot.name); //Should return 'Robo'

有人知道如何让这个工作吗?我听说需要编写

Object.prototype.extend = function(...);

但我不知道如何让这个系统工作。如果不可能,请告诉我另一个扩展对象的替代方案。


返回 true;但这就是我在问的原因 :) - Wituz
2
我建议您阅读MDN上关于面向对象JavaScript的精美教程:https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript - Pranav
如果在阅读了那些好文档之后,你仍然对extend函数感到好奇,我在这里设置了一个示例:http://jsfiddle.net/k9LRd/ - Codrin Eugeniu
4
我建议您不要严格将其视为“从Java转换为JavaScript”,而是视为“学习一种新语言,Javascript,它具有与Java类似的语法”。 - Toni Leigh
17个回答

206

你想要从Person的原型对象中继承:

var Person = function (name) {
    this.name = name;
    this.type = 'human';
};

Person.prototype.info = function () {
    console.log("Name:", this.name, "Type:", this.type);
};

var Robot = function (name) {
    Person.apply(this, arguments);
    this.type = 'robot';
};

Robot.prototype = Person.prototype;  // Set prototype to Person's
Robot.prototype.constructor = Robot; // Set constructor back to Robot

person = new Person("Bob");
robot = new Robot("Boutros");

person.info();
// Name: Bob Type: human

robot.info();
// Name: Boutros Type: robot

4
我有一个问题:当你执行new Robot()时,Person()构造函数是如何被调用的?在我的看法中,你应该调用基类构造函数而不是在Robot()构造函数中执行this.name = name;... - Alexis Wilke
23
是的,你应该调用 Person.apply(this, arguments);。同时最好使用 Robot.prototype = Object.create(Person.prototype); 而不是 new Person(); - Felix Kling
22
根据Felix的说法,“Robot.prototype = Person.prototype;”是一个不好的想法,如果有人希望“机器人”类型拥有自己的原型实例。添加新的针对机器人的特定功能也会将其添加到人类中。 - James Wilkins
24
这个例子完全是错的。这样做会改变Person的原型,这不是继承,并且有可能会在Person类中造成混乱。请参考建议使用Object.create()的答案,那才是正确的做法。 - nicolas-van
7
@osahyoun 这个回答在谷歌搜索中排名很高。我强烈建议你按其他评论中的建议修复代码并纠正原型链。 - raphaëλ
显示剩余12条评论

136

使用Object.create()的更简单的“类似散文”的语法

以及JavaScript的真正原型性质

*此示例已更新为ES6类和TypeScript。

首先,JavaScript是一种原型语言,而不是基于类的。它的真正本质在下面的原型形式中得以表达,你可能会发现它非常简单、类似散文,但却非常强大。

简而言之;

JavaScript

const Person = { 
    name: 'Anonymous', // person has a name
    greet() { console.log(`Hi, I am ${this.name}.`) } 
} 
    
const jack = Object.create(Person)   // jack is a person
jack.name = 'Jack'                   // and has a name 'Jack'
jack.greet()                         // outputs "Hi, I am Jack."

TypeScript
在TypeScript中,您需要设置接口,这些接口将在您创建Person原型的子类时进行扩展。一个名为politeGreet的变异显示了在子类jack上附加新方法的示例。
interface IPerson extends Object {
    name: string
    greet(): void
}

const Person: IPerson =  {
    name:  'Anonymous',  
    greet() {
        console.log(`Hi, I am ${this.name}.`)
    }
}

interface IPolitePerson extends IPerson {
    politeGreet: (title: 'Sir' | 'Mdm') => void
}

const PolitePerson: IPolitePerson = Object.create(Person)
PolitePerson.politeGreet = function(title: string) {
    console.log(`Dear ${title}! I am ${this.name}.`)
}

const jack: IPolitePerson = Object.create(Person)
jack.name = 'Jack'
jack.politeGreet = function(title): void {
    console.log(`Dear ${title}! I am ${this.name}.`)
}

jack.greet()  // "Hi, I am Jack."
jack.politeGreet('Sir') // "Dear Sir, I am Jack."

这样可以避免有时候复杂的构造模式。新对象继承自旧对象,但能够拥有自己的属性。如果我们尝试从新对象(`#greet()`)中获取一个新对象`jack`所缺少的成员,旧对象`Person`将提供该成员。
Douglas Crockford的话来说:“对象继承自对象。还有什么比这更面向对象的呢?” 你不需要构造函数,也不需要`new`实例化。你只需创建对象,然后扩展或改变它们。
这种模式还提供了部分或完全的不可变性,以及获取器/设置器
简洁明了。它的简单性并不妥协功能。请继续阅读。
创建一个`Person`原型的后代/副本(从技术上讲,比`class`更正确)。
*注意:以下示例是使用JS编写的。要使用Typescript编写,只需按照上面的示例设置接口进行类型定义。
const Skywalker = Object.create(Person)
Skywalker.lastName = 'Skywalker'
Skywalker.firstName = ''
Skywalker.type = 'human'
Skywalker.greet = function() { console.log(`Hi, my name is ${this.firstName} ${this.lastName} and I am a ${this.type}.`

const anakin = Object.create(Skywalker)
anakin.firstName = 'Anakin'
anakin.birthYear = '442 BBY'
anakin.gender = 'male' // you can attach new properties.
anakin.greet() // 'Hi, my name is Anakin Skywalker and I am a human.'

Person.isPrototypeOf(Skywalker) // outputs true
Person.isPrototypeOf(anakin) // outputs true
Skywalker.isPrototypeOf(anakin) // outputs true

如果你觉得直接赋值比使用构造函数更不安全,一种常见的方法是附加一个#create方法:
Skywalker.create = function(firstName, gender, birthYear) {

    let skywalker = Object.create(Skywalker)

    Object.assign(skywalker, {
        firstName,
        birthYear,
        gender,
        lastName: 'Skywalker',
        type: 'human'
    })

    return skywalker
}

const anakin = Skywalker.create('Anakin', 'male', '442 BBY')

Person原型分支到Robot

当你将Robot派生自Person原型时,不会影响Skywalkeranakin

// create a `Robot` prototype by extending the `Person` prototype:
const Robot = Object.create(Person)
Robot.type = 'robot'

附上Robot独有的附加方法。
Robot.machineGreet = function() { 
    /*some function to convert strings to binary */ 
}

// Mutating the `Robot` object doesn't affect `Person` prototype and its descendants
anakin.machineGreet() // error

Person.isPrototypeOf(Robot) // outputs true
Robot.isPrototypeOf(Skywalker) // outputs false

在TypeScript中,您还需要扩展Person接口:
interface Robot extends Person {
    machineGreet(): void
}
const Robot: Robot = Object.create(Person)
Robot.machineGreet = function() { console.log(101010) }

而且你可以使用混入(Mixins)——因为...达斯·维达(Darth Vader)是人类还是机器人?
const darthVader = Object.create(anakin)
// for brevity, property assignments are skipped because you get the point by now.
Object.assign(darthVader, Robot)

达斯·维达掌握了机器人的方法。
darthVader.greet() // inherited from `Person`, outputs "Hi, my name is Darth Vader..."
darthVader.machineGreet() // inherited from `Robot`, outputs 001010011010...

除了其他奇怪的事情之外:
console.log(darthVader.type) // outputs robot.
Robot.isPrototypeOf(darthVader) // returns false.
Person.isPrototypeOf(darthVader) // returns true.

这个优雅地反映了“现实生活”主观性:

“他现在更像机器而不是人,扭曲而邪恶。” - 奥比-旺·克诺比

“我知道你内心有善良。” - 卢克·天行者

与ES6之前的“经典”等效进行比较:

function Person (firstName, lastName, birthYear, type) {
    this.firstName = firstName 
    this.lastName = lastName
    this.birthYear = birthYear
    this.type = type
}

// attaching methods
Person.prototype.name = function() { return firstName + ' ' + lastName }
Person.prototype.greet = function() { ... }
Person.prototype.age = function() { ... }

function Skywalker(firstName, birthYear) {
    Person.apply(this, [firstName, 'Skywalker', birthYear, 'human'])
}

// confusing re-pointing...
Skywalker.prototype = Person.prototype
Skywalker.prototype.constructor = Skywalker

const anakin = new Skywalker('Anakin', '442 BBY')

// #isPrototypeOf won't work
Person.isPrototypeOf(anakin) // returns false
Skywalker.isPrototypeOf(anakin) // returns false

ES6 类

与使用对象相比稍显笨重,但代码可读性还可以:

class Person {
    constructor(firstName, lastName, birthYear, type) {
        this.firstName = firstName 
        this.lastName = lastName
        this.birthYear = birthYear
        this.type = type
    }
    name() { return this.firstName + ' ' + this.lastName }
    greet() { console.log('Hi, my name is ' + this.name() + ' and I am a ' + this.type + '.' ) }
}

class Skywalker extends Person {
    constructor(firstName, birthYear) {
        super(firstName, 'Skywalker', birthYear, 'human')
    }
}

const anakin = new Skywalker('Anakin', '442 BBY')

// prototype chain inheritance checking is partially fixed.
Person.isPrototypeOf(anakin) // returns false!
Skywalker.isPrototypeOf(anakin) // returns true

进一步阅读

可写性、可配置性和自由的获取器和设置器!

对于自由的获取器和设置器,或者额外的配置,您可以使用Object.create()的第二个参数,也称为propertiesObject。它也可以在#Object.defineProperty#Object.defineProperties中使用。

为了说明它的有用性,假设我们希望所有的机器人都严格由金属制成(通过writable: false),并标准化powerConsumption的值(通过获取器和设置器)。


// Add interface for Typescript, omit for Javascript
interface Robot extends Person {
    madeOf: 'metal'
    powerConsumption: string
}

// add `: Robot` for TypeScript, omit for Javascript.
const Robot: Robot = Object.create(Person, {
    // define your property attributes
    madeOf: { 
        value: "metal",
        writable: false,  // defaults to false. this assignment is redundant, and for verbosity only.
        configurable: false, // defaults to false. this assignment is redundant, and for verbosity only.
        enumerable: true  // defaults to false
    },
    // getters and setters
    powerConsumption: {
        get() { return this._powerConsumption },
        set(value) { 
            if (value.indexOf('MWh')) return this._powerConsumption = value.replace('M', ',000k') 
            this._powerConsumption = value
            throw new Error('Power consumption format not recognised.')
        }  
    }
})

// add `: Robot` for TypeScript, omit for Javascript.
const newRobot: Robot = Object.create(Robot)
newRobot.powerConsumption = '5MWh'
console.log(newRobot.powerConsumption) // outputs 5,000kWh

所有的Robot的原型都不能由其他材料制成:
const polymerRobot = Object.create(Robot)
polymerRobot.madeOf = 'polymer'
console.log(polymerRobot.madeOf) // outputs 'metal'

  • Object.create在许多(旧的)浏览器中不存在,特别是Internet Explorer 8及以下版本。
  • Object.create()仍然调用您通过它传递的函数的构造函数。
  • 对于每个属性声明,您都必须一遍又一遍地配置相同的设置(如示例代码所示)。
与使用“new”关键字相比,使用Object.create没有真正的好处。
- Harold
1
“古典训练”的程序员,你指的是什么? - Petra
1
我来自于一个经典的OOP思维模式,这个答案对我帮助很大。关于代码有两个问题: 1)今天的ES2015 Object.assign(Robot, {a:1} 是否是你的extend()方法的好替代品? 2)如何重写greet()方法,使其返回相同的文本,但附加了“ a greet override”? - Barry Staes
2
  1. #Object.assign 看起来是一个不错的替代方案。但目前浏览器支持较低。
  2. 你将使用对象的 __proto__ 属性来访问其原型的 greet 函数。然后你调用原型 greet 函数并传入调用者的作用域。在这种情况下,函数是一个控制台日志,所以无法“附加”。但通过这个例子,我想你可以理解。skywalker.greet = function() { this.__proto__.greet.call(this); console.log('a greet override'); }
- Calvintwr
1
那是应该与ECMAScript语言规范维护者进行讨论的问题。我基本上同意,但我必须使用我所拥有的东西来工作。 - user4639281
显示剩余13条评论

52
如果您还没有找到方法,请使用JavaScript对象的关联属性,按照下面所示的方式将扩展函数添加到Object.prototype中。
Object.prototype.extend = function(obj) {
   for (var i in obj) {
      if (obj.hasOwnProperty(i)) {
         this[i] = obj[i];
      }
   }
};
你可以像下面展示的那样使用此函数。

你可以像下面展示的那样使用此函数。

var o = { member: "some member" };
var x = { extension: "some extension" };

o.extend(x);

20
请注意,当在“父类”中使用对象/数组时,在“子类”中对其进行修改会创建指向原始对象的指针。具体来说,如果您在父类中有一个对象或数组,并在扩展该基类的子类中对其进行修改,则实际上会为所有扩展此相同基类的子类修改它。 - Harold
Harold,感谢你指出这一点。对于使用该函数的人来说,重要的是要加入一个条件来检查对象/数组并复制它们。 - tomilay

47

在ES6中,您可以使用展开运算符,例如:

var mergedObj = { ...Obj1, ...Obj2 };
请注意,Object.assign()会触发setter函数,而展开语法则不会。
有关更多信息,请参见链接:MDN- Spread Syntax

旧答案:

在ES6中,有Object.assign用于复制属性值。如果您不想修改目标对象(传递的第一个参数),请使用{}作为第一个参数。

var mergedObj = Object.assign({}, Obj1, Obj2);

了解更多详情请查看链接,MDN - Object.assign()

如果你需要的是 ES5 的 Polyfill ,该链接也提供了相应内容。 :)


31

不同的方法: Object.create

根据 @osahyoun 的回答,我发现以下是从 Person 的原型对象“继承”的更好和更有效的方式:

function Person(name){
    this.name = name;
    this.type = 'human';
}

Person.prototype.info = function(){
    console.log("Name:", this.name, "Type:", this.type);
}

function Robot(name){
    Person.call(this, name)
    this.type = 'robot';
}

// Set Robot's prototype to Person's prototype by
// creating a new object that inherits from Person.prototype,
// and assigning it to Robot.prototype
Robot.prototype = Object.create(Person.prototype);

// Set constructor back to Robot
Robot.prototype.constructor = Robot;

创建新实例:

var person = new Person("Bob");
var robot = new Robot("Boutros");

person.info(); // Name: Bob Type: human
robot.info();  // Name: Boutros Type: robot

现在,通过使用Object.create方法:
Person.prototype.constructor !== Robot

请查看MDN的文档。


2
只是想说@GaretClaborn它的工作是正确的,但你没有将“name”参数传递给父构造函数,就像这样:https://jsfiddle.net/3brm0a7a/3/(区别在第8行)。 - xPheRe
1
@xPheRe 哦,我明白了,谢谢。我已经编辑了答案以反映这个变化。 - That Realty Programmer Guy
1
@xPheRe,我想当我添加这个解决方案时,我更注重证明一个观点。谢谢。 - Lior Elrom
1
好的回答 +1,你可以看一下 ECMAScript 6。关键字 class 和 extends 可用:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain - Benjamin Poignant

19

再过一年,我可以告诉你还有另一个好的答案。

如果您不喜欢通过原型扩展对象/类的方式进行原型设计,请看看这个:https://github.com/haroldiedema/joii

以下是可能性的快速示例代码(以及更多):

var Person = Class({

    username: 'John',
    role: 'Employee',

    __construct: function(name, role) {
        this.username = name;
        this.role = role;
    },

    getNameAndRole: function() {
        return this.username + ' - ' + this.role;
    }

});

var Manager = Class({ extends: Person }, {

  __construct: function(name)
  {
      this.super('__construct', name, 'Manager');
  }

});

var m = new Manager('John');
console.log(m.getNameAndRole()); // Prints: "John - Manager"

好吧,我还有两个月才到两年期限 :P无论如何,JOII 3.0即将发布 :) - Harold
1
三年后再说。 - user3117575
有趣的概念,但语法看起来很丑陋。最好等待ES6类变得稳定。 - SleepyCal
我完全同意@sleepycal的观点。但不幸的是,在所有主要/常见的浏览器都实现这一点之前,至少还需要5年时间。因此,在那之前,这就是唯一的选择... - Harold

17

对于那些仍在寻找简单且最佳方法的人,您可以使用Spread Syntax扩展对象。

var person1 = {
      name: "Blank",
      age: 22
    };

var person2 = {
      name: "Robo",
      age: 4,
      height: '6 feet'
    };
// spread syntax
let newObj = { ...person1, ...person2 };
console.log(newObj.height);

注意: 请记住,最右边的属性具有优先级。在此示例中,person2 在右侧,因此 newObj 中将有名称为 Robo 的属性。


8

1
underscore.js的_.extend()的工作原理示例说明得非常清楚: http://lostechies.com/chrismissal/2012/10/05/extending-objects-in-underscore/ - Lemmings19

6

Mozilla宣布从ECMAScript 6.0中扩展对象:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends

注意:这是实验性技术,属于ECMAScript 6(Harmony)提案的一部分。

class Square extends Polygon {
  constructor(length) {
    // Here, it calls the parent class' constructor with lengths
    // provided for the Polygon's width and height
    super(length, length);
    // Note: In derived classes, super() must be called before you
    // can use 'this'. Leaving this out will cause a reference error.
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }

  set area(value) {
    this.area = value;     } 
}

这项技术在Gecko(Google Chrome / Firefox)- 03/2015夜间版中可用。

5

“纯JavaScript实现”并不是指环境提供的本地函数,它只使用了JavaScript来实现。 - binki
1
@binki,我的意思是原生JavaScript实现 - 这是ECMAScript 2015(ES6)标准的一部分。 - Cezary Daniel Nowak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接