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, ins开发者_如何学Gotead 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:
- You do something that looks up a property on the object. Say,
this.config
. - 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. - 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:
- Repeat step 3, continuing up (down?) the prototype chain as necessary.
- 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()
精彩评论