同时保留原始变量和解构的简洁方法

Clean way to keep original variable and destructure at the same time

是否有更简洁的方法来执行此操作(至少是 ES 草案并具有 babel 插件,即 ES6、ES7 等):

const { a, b } = result = doSomething();

我想将整体结果保留为一个单一对象,但同时又要对其进行解构。它在技术上有效,但 result 是隐式声明的(带有隐式 var),而我真的希望它也是一个常量。

我目前正在这样做:

const result = doSomething();
const { a, b } = result;

这又可以了,但是有点冗长,因为我需要重复这个模式几十次。

理想情况下,我想要的是:

const { a, b } = const result = doSomething();

但这显然是一个无效的语法。

一种可能的方式:

const result = doSomething(), 
    { a, b } = result;

不过,您仍然需要复制名称。 const 令牌不太方便。 )

想法 1

创建这个辅助函数:

function use(input, callback) {
    callback(input, input);
}

并像这样使用它:

use(doSomething(), (result, {a, b}) => {
    // Do something with result as a whole, or a and b as destructured properties.
});

例如:

use ({a: "Hello", b: "World", c: "!"}, (result, {a, b}) => {
  console.log(result);
  console.log(a);
  console.log(b);
});

// generates
// {a: "Hello", b: "World", c: "!"}
// Hello
// World

They're not const, but they're scoped, for better or worse!


想法 2

结合arrayobject解构。创建此辅助函数:

const dup = input => [input, input];

然后像这样解构:

const [result, {a, b}] = dup(doSomething());

Now, your result, a, and b are all consts.

In @raina77ow's answer they lament consttoken isn't quite right-handy; but if you use a colon (and repeat the keyword) instead of a comma, there's your answer.
But you already mentioned const result = doSomething(); const {a, b} = result; in your question, I dont see how it's any worse, and it works.

But from that, one thing you can see is that let something = x; let another = y; is the same as let [something, another] = [x, y];.
Thus a really elegant solution is actually simply:

const [result, {a, b}] = [,,].fill(doSomething());

You need the extra , as it is trailing



In addition to this (to make it it's own answer instead of only comment-worthy), this duplicating can also be done inside the destructuring syntaxt (which is why I came across this question).
Say b within result itself had a c; you want to destructure that, but also keep the reference to b.

//The above might lead you to believe you need to do this:
const result = doSomething(); const {a, b} = result; const {c} = b;
//or this
const [result, {a, b}, {b:{c}}] = [,,,].fill(doSomething());

But you can actually just

const [result, {a, b, b:{c}}] = [,,].fill(doSomething());

Now you have result, a, b, & c, even though a & b were in result, and c was in b.
This is especially handy if you dont actually need result, it looks like fill() is only required for the root object:
const {a, b, b:{c}} = doSomething();

This mightn't seem to work for arrays, since the position in the syntax is the key

const [result, [a, b, /*oops, I'm referencing index 2 now*/]] = [,,].fill(doArrayThing());

However, arrays are objects, so you can just use indices as keys and dupe an index reference:

const [result, {0:a, 1:b, 1:{c}}] = [,,].fill(doArrayThing());

This also means you can destructure array-likes, whereas normally it complains about the object not being iterable, and you can skip indices by just using a higher key instead of the array's syntax where you'll have to write empty commas.
And perhaps the best of all, {0:a, 1:b, ...c} still works as [a, b, ...c] would, since Object.keys() for an array pulls its indices (but the resulting c will not have a .length).



But I'm not content with that, and I really liked where @Arash was going with idea #2, but it wasn't generic enough to help with de-duping the b in the example above, and it dupes the const lines.

So...I wrote my own :| (ctrl+F for goodluck)
You use the same normal syntax, with some exceptions:

  • your destructure is written in a template literal, with the input object appearing as an interpolation
    eg [,,] = input becomes `[,,] = ${input}`
  • The equals is actually optional
  • you never rename the outputs within the destruction
    eg [a, b, ...c] = input becomes `[, , ...] ${input}`
  • the output of this template 运行 against μ (you can name it whatever) is an array of the elements you specified in order
    eg const {a:A, b:B} = input; becomes const [A,B] = μ`{a, b} ${input}`;
    NB how the rename comes at the output. And even if the input is an object, the output is always a flat array.
  • you can skip elements in an iterator using a number instead of repeated commas
    eg const [a, , , d] = input; is const [a,d] = μ`[ , 2, ]`;
  • and finally, the whole point of this; when going into an object, preceding it with a colon saves it to the output

eg

const [result, {a, b, b:{c}}] = [,,].fill(doSomething());

becomes

const [result, a, b] = μ`:{a, b::{c}} ${doSomething()}`;

So, other than the above, the pros:

  • I'm not 运行ning eval at all, but actually parsing and applying logic to your input,
    as such I can give you way better error messages at 运行time.

Eg ES6 doesnt even bother with this one:

_ = {a:7, get b() {throw 'hi'}};
console.warn('ES6');
out(() => {
    const {a, b} = _;
    return [a, b];
});
console.warn('hashbrown');
out(() => {
    const {a,b} = μ`{a,...} ${_}`;
    return [a, b];
});

Eg2 Here ES6 says _ was the culprit. Not only do I correctly say it was 1 at fault, but I tell you where in the destructure it happened:

_ = [1];
console.warn('ES6');
out(() => {
    const [[a]] = _;
    return [a];
});
console.warn('hashbrown');
out(() => {
    const [a] = μ`[[]] ${_}`;
    return [a];
});

  • super handy if you need to skip large arrays or are keeping a lot of inner variables

Eg

const [[a,,,,,,,,,j], [[aa, ab], [ba]]] = [,,].fill(_);
const [a, aa, ab, ba, j] = μ`[:[ , ], [ ], 7, ] ${_}`;

Okay, what's the catch? The cons:

  • well even that last pro, the destruction syntax with all the names missing can be hard to read. Really we need this syntax in the language, so the names are inside it instead of the const [ happening outside of it.
  • compilers dont know what to do with this, syntax errors are 运行time (whereas you'd be told earlier natively with ES6), IDE's probably wont be able to tell whats getting spat out (and I'm refusing to write a properly done templated .d.ts for it) if you're using some sort of typechecking
  • and as alluded to before you get slightly worse compile time errors for your syntax. I just tell you that something wasn't right, not what.
    But, to be fair, I still tell you where you went wrong, if you have multiple rest operators, I dont think ES6 is much help

Eg

_ = [1, 2, 3, 4];
console.warn('ES6');
out(() => {
    eval(`const [a, ...betwixt, b] = _`);
    return [a, betwixt, b];
});
console.warn('hashbrown');
out(() => {
    const [a, betwixt, b] = μ`[, ..., ] ${_}`;
    return [a, betwixt, b];
});

  • it's really only worth it if you're dealing with arrays, or you're renaming all the outputs anyway, because otherwise you're left specifying the name twice. This would be fixed along with point 1 if :{ :[ and [2 were adopted into the language, you wouldn't need to respecify outside in your const [
  • how I wrote it it honestly probably only 运行s in Chrome since firefox still doesn't have named capture groups. I was diligent in writing the [regex] parser to make all unused groups non-capturing, so if you're keen, it wouldn't be hard to make it FF-compatible

So where's the code?
You're keen.
Goodluck.

window.μ = (() => {
    //build regexes without worrying about
    // - double-backslashing
    // - adding whitespace for readability
    // - adding in comments
    let clean = (piece) => (piece
        .replace(/(?<=^|\n)(?<line>(?:[^\/\]|\/[^*\/]|\.)*)\/\*(?:[^*]|\*[^\/])*(\*\/|)/g, '$<line>')
        .replace(/(?<=^|\n)(?<line>(?:[^\/\]|\/[^\/]|\.)*)\/\/[^\n]*/g, '$<line>')
        .replace(/\n\s*/g, '')
    );
    let regex = ({raw}, ...interpolations) => (
        new RegExp(interpolations.reduce(
            (regex, insert, index) => (regex + insert + clean(raw[index + 1])),
            clean(raw[0])
        ))
    );

    let start = {
        parse : regex`^\s*(?:
            //the end of the string
            //I permit the equal sign or just declaring the input after the destructure definition without one
            (?<done>=?\s*)
            |
            //save self to output?
            (?<read>(?<save>:\s*|))
            //opening either object or array
            (?<next>(?<open>[{[]).*)
        )$`
    };
    let object = {
        parse : regex`^\s*
            (?<read>
                //closing the object
                (?<close>\})|

                //starting from open or comma you can...
                (?:[,{]\s*)(?:
                    //have a rest operator
                    (?<rest>\.\.\.)
                    |
                    //have a property key
                    (?<key>
                        //a non-negative integer
                        \b\d+\b
                        |
                        //any unencapsulated string of the following
                        \b[A-Za-z$_][\w$]*\b
                        |
                        //a quoted string
                        (?<quoted>"|')(?:
                            //that contains any non-escape, non-quote character
                            (?!\k<quoted>|\).
                            |
                            //or any escape sequence
                            (?:\.)
                        //finished by the quote
                        )*\k<quoted>
                    )
                    //after a property key, we can go inside
                    \s*(?<inside>:|)
                )
            )
            (?<next>(?:
                //after closing we expect either
                // - the parent's comma/close,
                // - or the end of the string
                (?<=\})\s*(?:[,}\]=]|$)
                |
                //after the rest operator we expect the close
                (?<=\.)\s*\}
                |
                //after diving into a key we expect that object to open
                (?<=:)\s*[{[:]
                |
                //otherwise we saw only a key, we now expect a comma or close
                (?<=[^:\.}])\s*[,}]
            ).*)
        $`,
        //for object, pull all keys we havent used
        rest : (obj, keys) => (
            Object.keys(obj)
                .filter((key) => (!keys[key]))
                .reduce((output, key) => {
                    output[key] = obj[key];
                    return output;
                }, {})
        )
    };
    let array = {
        parse : regex`^\s*
            (?<read>
                //closing the array
                (?<close>\])
                |
                //starting from open or comma you can...
                (?:[,[]\s*)(?:
                    //have a rest operator
                    (?<rest>\.\.\.)
                    |
                    //skip some items using a positive integer
                    (?<skip>\b[1-9]\d*\b)
                    |
                    //or just consume an item
                    (?=[^.\d])
                )
            )
            (?<next>(?:
                //after closing we expect either
                // - the parent's comma/close,
                // - or the end of the string
                (?<=\])\s*(?:[,}\]=]|$)
                |
                //after the rest operator we expect the close
                (?<=\.)\s*\]
                |
                //after a skip we expect a comma
                (?<=\d)\s*,
                |
                //going into an object
                (?<=[,[])\s*(?<inside>[:{[])
                |
                //if we just opened we expect to consume or consume one and close
                (?<=\[)\s*[,\]]
                |
                //otherwise we're just consuming an item, we expect a comma or close
                (?<=[,[])\s*[,\]]
            ).*)
        $`,
        //for 'array', juice the iterator
        rest : (obj, keys) => (Array.from(keys))
    };

    let destructure = ({next, input, used}) => {
//for exception handling
let phrase = '';
let debugging = () => {
    let tmp = type;
    switch (tmp) {
    case object: tmp = 'object'; break;
    case array : tmp = 'array'; break;
    case start : tmp = 'start'; break;
    }
    console.warn(
        `${tmp}\t%c${phrase}%c\u2771%c${next}`,
        'font-family:"Lucida Console";',
        'font-family:"Lucida Console";background:yellow;color:black;',
        'font-family:"Lucida Console";',
//input, used
    );
};
debugging = null;
        //this algorithm used to be recursive and beautiful, I swear,
        //but I unwrapped it into the following monsterous (but efficient) loop.
        //
        //Lots of array destructuring and it was really easy to follow the different parse paths,
        //now it's using much more efficient `[].pop()`ing.
        //
        //One thing that did get much nicer with this change was the error handling.
        //having the catch() rethrow and add snippets to the string as it bubbled back out was...gross, really
        let read, quoted, key, save, open, inside, close, done, rest, type, keys, parents, stack, obj, skip;
try {
        let output = [];
        while (
            //this is the input object and any in the stack prior
            [obj, ...parents] = input,
            //this is the map of used keys used for the rest operator
            [keys, ...stack] = used,
            //assess the type from how we are storing the used 'keys'
            type = (!keys) ? start : (typeof keys.next == 'function') ? array : object,
phrase += (read || ''),
read = '',
debugging && debugging(),
            //parse the phrase, deliberately dont check if it doesnt match; this way it will throw
            {read, quoted, next, key, save, open, inside, close, done, rest, skip} = next.match(type.parse).groups,
            done == null
        ) {
            if (open) {
                //THIS IS THE EXTRA FUNCTIONALITY
                if (save)
                    output.push(obj);
                switch (open) {
                case '{':
                    used = [{}, ...stack];
                    break;
                case '[':
                    used = [obj[Symbol.iterator](), ...stack];
                    input = [null, ...parents];
                    break;
                default:
                    throw open;
                }
                continue;
            }

            if (close) {
                used = stack;
                input = parents;
                continue;
            }
            //THIS IS THE EXTRA FUNCTIONALITY
            if (skip) {
                for (skip = parseInt(skip); skip-- > 0; keys.next());
                continue;
            }

            //rest operator
            if (rest) {
                obj = type.rest(obj, keys);
                //anticipate an immediate close
                input = [null, ...parents];
            }
            //fetch the named item
            else if (key) {
                if (quoted) {
                    key = JSON.parse(key);
                }
                keys[key] = true;
                obj = obj[key];
            }
            //fetch the next item
            else
                obj = keys.next().value;

            //dive into the named object or append it to the output
            if (inside) {
                input = [obj, ...input];
                used = [null, ...used];
            }
            else
                output.push(obj);
        }
        return output;
}
catch (e) {
    console.error('%c\u26A0 %cError destructuring', 'color:yellow;', '', ...input);
    console.error(
        `%c\u26A0 %c${phrase}%c${read || '\u2771'}%c${next || ''}`,
        'color:yellow;',
        'font-family:"Lucida Console";',
        'font-family:"Lucida Console";background:red;color:white;',
        'font-family:"Lucida Console";'
    );
    throw e;
}
return null;
    };
    //just to rearrange the inputs from template literal tags to what destructure() expects.
    //I used to have the function exposed directly but once I started supporting
    //iterators and spread I had multiple stacks to maintain and it got messy.
    //Now that it's wrapped it runs iteratively instead of recursively.
    return ({raw:[next]}, ...input) => (destructure({next, input, used:[]}));
})();

The demo's tests:

let out = (func) => {
    try {
        console.log(...func().map((arg) => (JSON.stringify(arg))));
    }
    catch (e) {
        console.error(e);
    }
};
let _;

//THE FOLLOWING WORK (AND ARE MEANT TO)
_ = {a:{aa:7}, b:8};
out(() => {
    const [input,{a,a:{aa},b}] = [,,].fill(_);
    return [input, a, b, aa];
});
out(() => {
    const [input,a,aa,b] = μ`:{a::{aa},b}=${_}`;
    return [input, a, b, aa];
});

_ = [[65, -4], 100, [3, 5]];
out(() => {
    //const [[aa, ab], , c] = input; const [ca, cb] = c;
    const {0:{0:aa, 1:ab}, 2:c, 2:{0:ca, 1:cb}} = _;
    return [aa, ab, c, ca, cb];
});
out(() => {
    const [aa,ab,c,ca,cb] = μ`{0:{0,1}, 2::{0,1}}=${_}`;
    return [aa, ab, c, ca, cb];
});

_ = {a:{aa:7, ab:[7.5, 7.6, 7.7], 'a c"\'':7.8}, b:8};
out(() => {
    const [input,{a,a:{aa,ab,ab:{0:aba, ...abb},"a c\"'":ac},b,def='hi'}] = [,,].fill(_);
    return [input, a, aa, ab, aba, abb, ac, b, def];
});
out(() => {
    const [input,a,aa,ab,aba,abb,ac,b,def='hi'] = μ`:{a::{aa,ab::{0, ...},"a c\"'"},b}=${_}`;
    return [input, a, aa, ab, aba, abb, ac, b, def];
});

_ = [{aa:7, ab:[7.5, {abba:7.6}, 7.7], 'a c"\'':7.8}, 8];
out(() => {
    const [input,[{aa,ab,ab:[aba,{abba},...abc],"a c\"'":ac}],[a,b,def='hi']] = [,,,].fill(_);
    return [input, a, aa, ab, aba, abba, abc, ac, b, def];
});
out(() => {
    const [input,a,aa,ab,aba,abba,abc,ac,b,def='hi'] = μ`:[:{aa,ab::[,{abba},...],"a c\"'"},]=${_}`;
    return [input, a, aa, ab, aba, abba, abc, ac, b, def];
});

_ = [[-1,-2],[-3,-4],4,5,6,7,8,9,0,10];
out(() => {
    const [[a,,,,,,,,,j], [[aa, ab], [ba]]] = [,,].fill(_);
    return [a, aa, ab, ba, j];
});
out(() => {
    const [a, aa, ab, ba, j] = μ`[:[ , ], [ ], 7, ] ${_}`;
    return [a, aa, ab, ba, j];
});


//THE FOLLOWING FAIL (AND ARE MEANT TO)

_ = [1];
console.warn('ES6');
out(() => {
    const [[a]] = _;
    return [a];
});
console.warn('hashbrown');
out(() => {
    const [a] = μ`[[]] ${_}`;
    return [a];
});


_ = [1, 2, 3, 4];
console.warn('ES6');
out(() => {
    eval(`const [a, ...betwixt, b] = _`);
    return [a, betwixt, b];
});
console.warn('hashbrown');
out(() => {
    const [a, betwixt, b] = μ`[, ..., ] ${_}`;
    return [a, betwixt, b];
});


_ = {a:7, get b() {throw 'hi'}};
console.warn('ES6');
out(() => {
    const {a, b} = _;
    return [a, b];
});
console.warn('hashbrown');
out(() => {
    const {a,b} = μ`{a,...} ${_}`;
    return [a, b];
});

And the output if your browser couldn't 运行 it but you're curious (the errors are testing error outputs for native vs this thing)