[removed] How can I mix in methods of other Object

2019-07-29 03:06发布

问题:

If I create an Object A:

let A = {};

And want to mix in methods from other Objects B and C:

let B = {
    foo() {
        alert("Boo!");
    }
};
let C = {
    bar() {
        alert("No!");
    }
};

Normally I would call:

Object.assign(A, B, C); Then I change my function foo:

Object.assign(B, {
    foo() {
        alert("Hooray!");
    }
});
Objcect.assign(C, {
    bar() {
        alert("Yes!");
    }
});

After that I call foo or bar:

A.foo(); // Actual output: "Boo!", desired: "Hooray!"
A.bar(); // Actual output: "No!",  desired: "Yes!"

So far I found out, that Object.assign only copies methods in the target, but it doesn't link them. I already uploaded a question concerning mixing in only one Object, which was solved: Mix in one object, solution: prototypical inheritance

About inheritance and composition I found a useful blogposts here: Understanding Prototypes, Delegation & Composition

I want to mix methods in an Object, but not copy the methods, much more rather I want an assignments to the definitions of the mixed in functions.

How is this possible? (Maybe in ES2015?)

回答1:

Two answers to this:

They're not copied, they're referenced

I want to mix methods in an Object, but not copy the methods...

What you're doing isn't copying the methods (functions; JavaScript doesn't really have methods), it's reusing the ones you already have. Only a single foo or bar function exists; there are just properties on A and B, and on A and C, that both refer to that same function.

You can see that from the fact that A.foo === B.foo and A.bar === C.bar:

let B = {
    foo() {
        alert("Boo!");
    }
};
let C = {
    bar() {
        alert("No!");
    }
};
let A = Object.assign({}, B, C);
snippet.log(A.foo === B.foo); // true
snippet.log(A.bar === C.bar); // true
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="//tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

What your code produces in memory looks like this (with some details omitted):

                        +----------+  
                    B>->| (object) |  
                        +----------+     +----------------+
                        | foo      |>-+->|   (function)   |
                        +----------+  |  +----------------+
                                      |  | (code for foo) |
                  +-------------------+  +----------------+
                  |
                  |     +----------+      
                  | C>->| (object) |      
                  |     +----------+     +----------------+
                  |     | bar      |>-+->|   (function)   |
                  |     +----------+  |  +----------------+
                  |                   |  | (code for bar) |
                  |                   |  +----------------+
                  |                   |
    +----------+  |                   |
A>->| (object) |  |                   |
    +----------+  |                   |
    | foo      |>-+                   |
    | bar      |>---------------------+
    +----------+

But if you really want a link

If you want to be able to replace the B.foo or C.bar function with something else, and have A see that change, you can do that by making foo and bar on A property accessors:

let A = {
    get foo() { return B.foo; },
    get bar() { return C.bar; }
};

Now A is linked to B and C:

let B = {
    foo() {
      snippet.log("foo1");
    }
};
let C = {
    bar() {
      snippet.log("bar1");
    }
};

let A = {
    get foo() { return B.foo; },
    get bar() { return C.bar; }
};

A.foo(); // foo1
A.bar(); // bar1

B.foo = function() {
    snippet.log("foo2");
};

A.foo(); // foo2

C.bar = function() {
    snippet.log("bar2");
};

A.bar(); // bar2
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="//tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

You can do that for a dynamic mix of properties using Object.defineProperty:

function linkMethods(to, ...from) {
    from.forEach(source => {
        for (let key in source) { // We're intentionally not filtering out inherited
            if (typeof source[key] === "function") {
                Object.defineProperty(to, key, {
                    get: function() {
                        return source[key];
                    }
                });
            }
        }
    });
    return to;
}

In action:

function linkMethods(to, ...from) {
    from.forEach(source => {
        for (let key in source) { // We're intentionally not filtering out inherited
            if (typeof source[key] === "function") {
                Object.defineProperty(to, key, {
                    get: function() {
                        return source[key];
                    }
                });
            }
        }
    });
}

let B = {
    foo() {
      snippet.log("foo1");
    }
};
let C = {
    bar() {
      snippet.log("bar1");
    }
};

let A = {};
linkMethods(A, B);
linkMethods(A, C);

A.foo(); // foo1
A.bar(); // bar1

B.foo = function() {
    snippet.log("foo2");
};

A.foo(); // foo2

C.bar = function() {
    snippet.log("bar2");
};

A.bar(); // bar2
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="//tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Despite my use of ES2015 arrow functions above, if you convert those to function functions, all of the above works in ES5 (but not ES3 or earlier, which didn't have property accessors).


Re your comment:

Could you please explain what the difference between copying, a reference and a link is?

A couple of key things:

  1. In JavaScript, functions are objects. Genuine, real objects that have all of the features of other kinds of objects, and also the ability to contain and run code. (This is frequently not the case in other languages.)

  2. Variables and properties hold values.

  3. A value is either a primitive (like 1 or "foo"), or an object reference. The object reference isn't the object, it's just a reference to the object, which exists elsewhere.

  4. When you assign a value to a variable or property, you're copying the value. If the value is an object reference, that means you're copying the reference, not the object.

  5. JavaScript doesn't have (at the external level) either variable references or property references. (Most languages don't, but some do.)

I like to explain object references, variables, and values like this: Say we have this guy Joe. Joe's really happy because he's turning 42 today, and he's a fan of The Hitchhiker's Guide to the Galaxy. So he writes the number 42 on a piece of paper. The 42 is a value, in this case a number. The piece of paper is a variable or property.

Joe decides to have a party, so he puts up a party announcement on the break room notice board at his job saying he's having a party and writes his home address on it. The announcement is also a variable or property. Joe's house is an object. The address written on the announcement is an object reference, telling us where Joe's house is.

Joe runs into Mohammed in the break room, and knowing he's a fellow HHG fan, shows him his paper with 42 on it, and points out the party announcement. Mohammed gets out a piece of paper and copies the 42 onto it, to remember how old Joe's turning (perhaps to buy the appropriate card). That is, he copies the 42 (the value) onto his piece of paper (the variable/property). Then, because he hasn't been to Joe's house before, he gets another piece of paper and copies the house's address from Joe's party announcement. The house isn't copied, just the address. The object wasn't copied, just the reference to it.

Later, Joe realizes the party's getting too big for his house, and decides to move it to a pub in the city. He crosses out the address on the announcement and writes in the address of the pub. But he forgets to tell Mohammed. So come party time, Mohammed goes to Joe's house instead of the pub.

Back to JavaScript and functions, which are objects:

If you have:

let B = {
    foo() {
        console.log("I'm foo");
    }
};

B.foo doesn't contain the foo function, it contains a value (an object reference) that refers to the foo function. Then when you do:

let A = {};
A.foo = B.foo;

...you copy the value (the object reference) from B.foo into A.foo. There is no connection, no link whatsoever between A.foo and B.foo other than that they happen to contain the same value, just like there's no connection between Joe's party announcement and Mohammed's piece of paper, other than that they happen to both have Joe's address on them (to start with).

Later, if you do B.foo = function() { /*...*/ };, you're replacing the value in B.foo with a new value that refers to a different function. This has no effect on A.foo because, again, there's no link between A.foo and B.foo. Just like it didn't have any effect on Mohammed's piece of paper when Joe crossed out the address on the party announcement and wrote in the address of the pub.

The reason my property accessor mechanism above works is that a property accessor is a function which gets run every time you read the property's value. So when you get the value of A.foo, it really runs a function that returns the value of B.foo as it is then, not as it was earlier. The analogy would be Mohammed writing down "look at the announcement" on his piece of paper instead of writing down Joe's address, and then when getting ready to go to the party, looking at his piece of paper to remember where to go, seeing "look at the announcement," going back to the break room, and seeing the updated address — and thus going to the pub.



回答2:

You can use Proxies to create mixins:

let A = {
    aa: "AA",
    bb: 'BB'

};

let B = {
    foo() {
        console.log(this.aa);
    }
};
let C = {
    bar() {
        console.log(this.bb);
    }
};


mixin = function(target, ...others) {

  return new Proxy(target, {
    others,
    get(target, prop) {
       for (let p of this.others)
          if (prop in p)
             return p[prop];
        return target[prop];      
    }
  });
}  


let mA = mixin(A, B, C);
mA.foo();
mA.bar();

This can be optimized by indexing properties to avoid the lookup each time.