简体   繁体   中英

Define a function, extend the functions prototype, create two instances, prototype was modified?

Can someone please educate me on why the result is what it is, instead of what I expected it to be. This is driving me nuts!

var f = function(b){
    console.log(this.config);
    this.config.b = b;
}
f.prototype.config = {
    a: 'a',
    b: 'b'
};

var f1 = new f('bb');
var f2 = new f('bbb');

// logs
// { a: 'a', b: 'b' }
// { a: 'a', b: 'bb' }

// expected
// { a: 'a', b: 'b' }
// { a: 'a', b: 'b' }

It's not the prototype that's being modified, but rather the config object you've put on the prototype. This is correct behavior, objects referenced by the prototype are not copied when you create a new instance. f1.config === f2.config , they point to the same object.

The way the prototype chain works for get operations is this:

  1. You do something that looks up a property on the object. Say, this.config .
  2. The object itself is checked to see if it has a property by that name. If so, that copy of the property is used and its value is returned. In your case, the object doesn't have its own config , so we continue with the next step.
  3. The object's prototype is checked to see if it has the property. If so, that copy of the property is used and its value is returned. In your case, this is true, because the prototype has the property. But just for completeness:
  4. Repeat step 3, continuing up (down?) the prototype chain as necessary.
  5. If the property isn't found at all , return undefined .

( set operations work differently; a set operation always updates or creates a property on the object it's being set on, never further down [up?] the prototype chain.)

So in your case, since your instances don't have a config property we go to the prototype. Since the prototype does have a config property, it's used. The value of the property is an object reference, and so if you change the object (by assigning to one of its properties), it's changed and anything else that also uses that object will see the change.

Another way to look at it is to do a graph:

+------+       +------+
|  f1  |       |  f2  |
+------+       +------+
   |              |
   +------+-------+
          |
          v
    +--------------------+       +--------+
    | [[Proto]] assigned |       | config |
    | via `new f`        |------>| object |
    +--------------------+       +--------+
                                     |
                             +-------+-------+
                             |               |
                             V               v
                     +------------+     +------------+
                     | a property |     | b property |
                     +------------+     +------------+

Another way to think of it is to get the function and prototype out of the way entirely:

var pseudoProto = {};               // A stand-in for the prototype...
pseudoProto.config = {              // ...with `config` on it
    a: 'a',
    b: 'b'
};
var f1 = {};                        // A blank object...
f1.pseudo = pseudoProto;            // ...referencing `pseudoProto`
var f2 = {};                        // Another blank object...
f2.pseudo = pseudoProto;            // ...also referencing `pseudoProto`
f1.pseudo.config.b = "bb";          // Change the `b` property on `config`
console.log(f2.pseudo.config.b);    // Logs "bb", of course

In a very real way, that's what's happening under the covers via new f() . You can't directly access the property of the f1 and f2 instances that points to the prototype (the spec calls it the [[Proto]] property), but it's a real thing, and it's really there. [FYI: The latest version of the ECMAScript spec lets us do a few things directly with the [[Proto]] property, like create raw objects with a specific [[Proto]] (without going via a function), but still doesn't give us direct access to the property itself.]

There are plenty of times you want all instances to share the same object (function objects, for instance!), and so the prototype is the right place for those object references to be; but if you want each instance to have its own copy of the object, you want to create that object in the constructor function. In your case:

var f = function(b){
    this.config = {
        a: 'a',
        b: b
    };
}

var f1 = new f('bb');
console.log(f1.config);
var f2 = new f('bbb');
console.log(f2.config);
// Logs
// { a: 'a', b: 'bb' }
// { a: 'a', b: 'bbb' }

(Note I moved the console.log statements, so we see the result at the end rather than an intermediate state.)

Here is a good coffeescript example which showcases the problem:

Person = class
  config:
    name: "sin nombre"
  constructor: (config={}) ->
    @config.name = config.name  if config.name?
  getName: -> @config.name

Human = class extends Person

ben = new Human(name: 'Ben')
sonny = new Human()
alert ben.getName()
alert sonny.getName()

and the solution:

Person = class
  config:
    name: "sin nombre"
  constructor: (config={}) ->
    @config = {}
    @config[key] = value  for own key, value of Person::config
    @config.name = config.name  if config.name?
  getName: -> @config.name

Human = class extends Person

ben = new Human(name: 'Ben')
sonny = new Human()
alert ben.getName()
alert sonny.getName()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM