关于javascript:使用“Object.create”而不是“new”

Using “Object.create” instead of “new”


Javascript 1.9.3 / ECMAScript 5介绍了Object.create,Douglas Crockford等人长期以来一直在倡导。 如何用Object.create替换下面代码中的new

1
2
3
4
5
6
7
8
9
var UserA = function(nameParam) {
    this.id = MY_GLOBAL.nextId();
    this.name = nameParam;
}
UserA.prototype.sayHello = function() {
    console.log('Hello '+ this.name);
}
var bob = new UserA('bob');
bob.sayHello();


(假设存在MY_GLOBAL.nextId)。


我能想到的最好的是:

1
2
3
4
5
6
7
8
9
10
11
12
var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var bob = Object.create(userB);
bob.init('Bob');
bob.sayHello();


似乎没有任何优势,所以我想我没有得到它。 我可能太新古典了。 我应该如何使用Object.create创建用户'bob'?



只有一个级别的继承,您的示例可能无法让您看到Object.create的真正好处。


此方法允许您轻松实现差异继承,其中对象可以直接从其他对象继承。


userB示例中,我认为您的init方法不应该是公共的,甚至不存在,如果在现有对象实例上再次调用此方法,idname属性将会更改。


Object.create允许您使用其第二个参数初始化对象属性,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var userB = {
  sayHello: function() {
    console.log('Hello '+ this.name);
  }
};

var bob = Object.create(userB, {
  'id' : {
    value: MY_GLOBAL.nextId(),
    enumerable:true // writable:false, configurable(deletable):false by default
  },
  'name': {
    value: 'Bob',
    enumerable: true
  }
});


如您所见,属性可以在Object.create的第二个参数上初始化,对象文字使用类似于Object.definePropertiesObject.defineProperty方法使用的语法。


它允许您设置属性属性(enumerablewritableconfigurable),这可能非常有用。



使用Object.create(...)而不是new object确实没有优势。


那些提倡这种方法的人通常会说出相当模糊的优点:"可扩展性",或"JavaScript更自然"等。


但是,我还没有看到一个具体的例子,表明Object.create比使用new有任何优势。相反,它存在已知的问题。 Sam Elsamman描述了当嵌套对象和Object.create(...)被使用时会发生什么:

1
2
3
4
5
6
7
8
var Animal = {
    traits: {},
}
var lion = Object.create(Animal);
lion.traits.legs = 4;
var bird = Object.create(Animal);
bird.traits.legs = 2;
alert(lion.traits.legs) // shows 2!!!


发生这种情况是因为Object.create(...)提倡使用数据创建新对象的做法;这里Animal数据成为lionbird原型的一部分,并在共享时引起问题。使用new时,原型继承是明确的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Animal() {
    this.traits = {};
}

function Lion() { }
Lion.prototype = new Animal();
function Bird() { }
Bird.prototype = new Animal();

var lion = new Lion();
lion.traits.legs = 4;
var bird = new Bird();
bird.traits.legs = 2;
alert(lion.traits.legs) // now shows 4


关于传递到Object.create(...)的可选属性属性,可以使用Object.defineProperties(...)添加这些属性。



Object.create还不是几个浏览器的标准,例如IE8,Opera v11.5,Konq 4.3没有它。您可以为这些浏览器使用Douglas Crockford的Object.create版本,但这不包括CMS答案中使用的第二个"初始化对象"参数。


对于跨浏览器代码,在此期间获得对象初始化的一种方法是定制Crockford的Object.create。这是一种方法: -

1
2
3
4
5
6
7
8
9
10
Object.build = function(o) {
   var initArgs = Array.prototype.slice.call(arguments,1)
   function F() {
      if((typeof o.init === 'function') && initArgs.length) {
         o.init.apply(this,initArgs)
      }
   }
   F.prototype = o
   return new F()
}


这维护了Crockford原型继承,并检查对象中的任何init方法,然后使用您的参数运行它,比如说new man('John','Smith')。你的代码变成: -

1
2
3
4
5
6
7
8
9
10
11
12
13
MY_GLOBAL = {i: 1, nextId: function(){return this.i++}}  // For example

var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var bob = Object.build(userB, 'Bob');  // Different from your code
bob.sayHello();


所以bob继承了sayHello方法,现在有自己的属性id = 1和name ='Bob'。当然,这些属性都是可写的和可枚举的。这也是一种比ECMA Object.create更简单的初始化方法,特别是如果您不关心可写,可枚举和可配置的属性。


对于没有init方法的初始化,可以使用以下Crockford mod: -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.gen = function(o) {
   var makeArgs = arguments
   function F() {
      var prop, i=1, arg, val
      for(prop in o) {
         if(!o.hasOwnProperty(prop)) continue
         val = o[prop]
         arg = makeArgs[i++]
         if(typeof arg === 'undefined') break
         this[prop] = arg
      }
   }
   F.prototype = o
   return new F()
}


这将按照定义的顺序在userB参数之后使用Object.gen参数从左到右填充userB自己的属性。它使用for(prop in o)循环,因此,根据ECMA标准,属性枚举的顺序不能保证与属性定义的顺序相同。但是,在(4)主要浏览器上测试的几个代码示例显示它们是相同的,只要使用hasOwnProperty过滤器,有时即使不使用。

1
2
3
4
5
6
7
8
9
10
11
MY_GLOBAL = {i: 1, nextId: function(){return this.i++}};  // For example

var userB = {
   name: null,
   id: null,
   sayHello: function() {
      console.log('Hello '+ this.name);
   }
}

var bob = Object.gen(userB, 'Bob', MY_GLOBAL.nextId());


我会说比Object.build更简单,因为userB不需要init方法。 userB也不是一个特定的构造函数,但看起来像一个普通的单例对象。因此,使用此方法,您可以从普通的普通对象构造和初始化。



TL; DR:


new Computer()将调用构造函数Computer(){}一次,而Object.create(Computer.prototype)则不会。


所有优点都基于这一点。


关于性能的旁注:构造函数调用如new Computer()由引擎进行了大量优化,因此它可能比Object.create更快。



您可以使init方法返回this,然后将调用链接在一起,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
        return this;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};

var bob = Object.create(userB).init('Bob');


Object.create的另一个可能用法是以便宜有效的方式克隆不可变对象。

1
2
3
4
5
6
7
8
9
10
11
var anObj = {
    a:"test",
    b:"jest"
};

var bObj = Object.create(anObj);

bObj.b ="gone"; // replace an existing (by masking prototype)
bObj.c ="brand"; // add a new to demonstrate it is actually a new obj

// now bObj is {a: test, b: gone, c: brand}


注意:上面的代码片段创建了一个源对象的克隆(也就是cObj = aObj中的引用,而不是引用)。它比copy-properties方法(参见1)更有优势,因为它不会复制对象成员属性。相反,它创建了另一个-destination-对象,并在源对象上设置了原型。此外,当在dest对象上修改属性时,它们是"动态"创建的,掩盖了原型(src)的属性。这构成了克隆不可变对象的快速有效方法。


这里需要注意的是,这适用于创建后不应修改的源对象(不可变)。如果在创建后修改了源对象,则也将修改所有克隆的未屏蔽属性。


这里小提琴(http://jsfiddle.net/y5b5q/1/)(需要具有Object.create功能的浏览器)。



我认为主要问题是理解newObject.create方法之间的区别。根据这个答案和这个视频new关键字做下一件事:


  • 创建新对象。


  • 将新对象链接到构造函数(prototype)。


  • 使this变量指向新对象。


  • 使用新对象执行构造函数并隐式执行return this;


  • 将构造函数名称分配给新对象的属性constructor


  • Object.create仅执行1st2nd步骤!


    在提供的代码示例中,这不是什么大问题,但在下一个示例中它是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var onlineUsers = [];
    function SiteMember(name) {
        this.name = name;
        onlineUsers.push(name);
    }
    SiteMember.prototype.getName = function() {
        return this.name;
    }
    function Guest(name) {
        SiteMember.call(this, name);
    }
    Guest.prototype = new SiteMember();

    var g = new Guest('James');
    console.log(onlineUsers);


    副作用结果将是:

    1
    [ undefined, 'James' ]


    因为Guest.prototype = new SiteMember();
    但是我们不需要执行父构造函数方法,我们只需要让方法getName在Guest中可用。
    因此我们必须使用Object.create
    如果替换Guest.prototype = new SiteMember();
    Guest.prototype = Object.create(SiteMember.prototype);结果是:

    1
    [ 'James' ]


    有时您无法使用NEW创建对象,但仍然可以调用CREATE方法。


    例如:如果要定义自定义元素,则必须从HTMLElement派生。

    1
    2
    3
    proto = new HTMLElement  //fail :(
    proto = Object.create( HTMLElement.prototype )  //OK :)
    document.registerElement("custom-element", { prototype: proto } )


    优点是在大多数浏览器中Object.create通常比new


    在这个jsperf示例中,在Chromium中,浏览器new的速度是Object.create(obj)的30倍,尽管两者都非常快。这一切都很奇怪,因为new会做更多事情(比如调用构造函数),其中Object.create应该只是创建一个新的Object,传入的对象作为原型(Crockford中的秘密链接)


    也许浏览器还没有提高Object.create的效率(也许它们基于new的底层......甚至在本机代码中)



    newObject.create用于不同的目的。 new旨在创建对象类型的新实例。 Object.create旨在简单地创建一个新对象并设置其原型。为什么这有用?在不访问__proto__属性的情况下实现继承。对象实例的原型称为[[Prototype]],是虚拟机的内部属性,不应直接访问。实际上可以直接访问[[Prototype]]作为__proto__属性的唯一原因是因为它一直是每个主要虚拟机实现ECMAScript的事实上的标准,并且此时删除它会破坏很多现有代码。


    在回应上面7ochem的回答时,对象绝对不应该将它们的原型设置为new语句的结果,这不仅是因为没有多少次调用相同的原型构造函数,而且因为同一类的两个实例可以如果在创建之后修改了一个原型,最终会产生不同的行为。由于误解和打破原型继承链的预期行为,这两个示例都只是错误的代码。


    不是访问__proto__,而是应该使用Object.create创建实例的原型,或者之后使用Object.setPrototypeOf创建实例的原型,并使用Object.getPrototypeOfObject.isPrototypeOf进行读取。


    另外,正如Object.setPrototypeOf的Mozilla文档所指出的那样,在创建对象的原型之后,出于性能原因修改对象的原型是一个坏主意,此外还有一个事实是在创建对象的原型之后修改对象的原型会导致未定义如果访问它的给定代码片段可以在修改原型之后的OR之前执行,除非该代码非常小心地检查当前原型或不访问两者之间不同的任何属性。


    特定
    const X = function (v) { this.v = v };
    X.prototype.whatAmI = 'X';
    X.prototype.getWhatIAm = () => this.whatAmI;
    X.prototype.getV = () => this.v;

    以下VM伪代码等同于语句const x0 = new X(1);
    const x0 = {};
    x0.[[Prototype]] = X.prototype;
    X.prototype.constructor.call(x0, 1);

    请注意,虽然构造函数可以返回任何值,但new语句始终忽略其返回值并返回对新创建的对象的引用。
    以下伪代码等同于语句const x1 = Object.create(X.prototype);
    const x0 = {};
    x0.[[Prototype]] = X.prototype;

    正如您所看到的,两者之间的唯一区别是Object.create不执行构造函数,构造函数实际上可以返回任何值,但如果没有另外指定则只返回新的对象引用this


    现在,如果我们想要使用以下定义创建子类Y:
    const Y = function(u) { this.u = u; }
    Y.prototype.whatAmI = 'Y';
    Y.prototype.getU = () => this.u;

    然后我们可以通过写入__proto__使它从X继承:
    Y.prototype.__proto__ = X.prototype;
    虽然可以在不写入__proto__的情况下完成同样的事情:
    Y.prototype = Object.create(X.prototype);
    Y.prototype.constructor = Y;

    在后一种情况下,有必要设置原型的构造函数属性,以便new Y语句调用正确的构造函数,否则new Y将调用函数X。如果程序员确实希望new Y调用X,那么在Y的构造函数中使用X.call(this, u)可以更好地完成



    虽然Douglas Crockford曾经是Object.create()的热心倡导者,但他基本上就是这个结构实际上是javascript的原因,他不再有这个观点。


    他停止使用Object.create,因为他完全停止使用这个关键字,因为它会造成太多麻烦。例如,如果你不小心它可以很容易地指向全局对象,这可能会产生非常糟糕的后果。他声称不使用这个Object.create就没有意义了。


    您可以查看2014年他在Nordic.js会谈的视频:



    enter image description here



    您必须创建自定义Object.create()功能。一个解决Crockfords问题,也调用你的init函数。


    这将有效:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var userBPrototype = {
        init: function(nameParam) {
            this.name = nameParam;
        },
        sayHello: function() {
            console.log('Hello '+ this.name);
        }
    };


    function UserB(name) {
        function F() {};
        F.prototype = userBPrototype;
        var f = new F;
        f.init(name);
        return f;
    }

    var bob = UserB('bob');
    bob.sayHello();


    这里UserB就像Object.create,但根据我们的需要进行了调整。


    如果需要,您也可以致电:

    1
    var bob = new UserB('bob');


    摘要:

    • Object.create()是一个Javascript函数,它接受2个参数并返回一个新对象。
    • 第一个参数是一个对象,它将成为新创建的对象的原型
    • 第二个参数是一个对象,它将是新创建的对象的属性

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const proto = {
      talk : () => console.log('hi')
    }

    const props = {
      age: {
        writable: true,
        configurable: true,
        value: 26
      }
    }


    let Person = Object.create(proto, props)

    console.log(Person.age);
    Person.talk();

    实际应用:

  • 以这种方式创建对象的主要优点是可以显式定义原型。使用对象文字或new关键字时,您无法控制它(但是,当然可以覆盖它们)。
  • 如果我们想要一个原型new关键字调用一个构造函数。使用Object.create(),无需调用甚至声明构造函数。
  • 当您想要以非常动态的方式创建对象时,它基本上可以是一个有用的工具。我们可以创建一个对象工厂函数,它根据收到的参数创建具有不同原型的对象。


  • 我更喜欢封闭式方法。


    我仍然使用new
    我不使用Object.create
    我不使用this


    我仍然使用new,因为我喜欢它的声明性质。


    考虑这个简单的继承。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    window.Quad = (function() {

        function Quad() {

            const wheels = 4;
            const drivingWheels = 2;

            let motorSize = 0;

            function setMotorSize(_) {
                motorSize = _;
            }

            function getMotorSize() {
                return motorSize;
            }

            function getWheelCount() {
                return wheels;
            }

            function getDrivingWheelCount() {
                return drivingWheels;
            }
            return Object.freeze({
                getWheelCount,
                getDrivingWheelCount,
                getMotorSize,
                setMotorSize
            });
        }

        return Object.freeze(Quad);
    })();

    window.Car4wd = (function() {

        function Car4wd() {
            const quad = new Quad();

            const spareWheels = 1;
            const extraDrivingWheels = 2;

            function getSpareWheelCount() {
                return spareWheels;
            }

            function getDrivingWheelCount() {
                return quad.getDrivingWheelCount() + extraDrivingWheels;
            }

            return Object.freeze(Object.assign({}, quad, {
                getSpareWheelCount,
                getDrivingWheelCount
            }));
        }

        return Object.freeze(Car4wd);
    })();

    let myQuad = new Quad();
    let myCar = new Car4wd();
    console.log(myQuad.getWheelCount()); // 4
    console.log(myQuad.getDrivingWheelCount()); // 2
    console.log(myCar.getWheelCount()); // 4
    console.log(myCar.getDrivingWheelCount()); // 4 - The overridden method is called
    console.log(myCar.getSpareWheelCount()); // 1


    鼓励反馈。