Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6解构赋值运算符 #24

Open
bencode opened this issue Dec 8, 2017 · 0 comments
Open

ES6解构赋值运算符 #24

bencode opened this issue Dec 8, 2017 · 0 comments

Comments

@bencode
Copy link

bencode commented Dec 8, 2017

本次我领到的任务是:

在ES6中有一个解构赋值运算符,可以大大方便数据字段的获取。 比如

const [a, b] = [1, 2, 3];
const {name, age} = {name: 'helijia', age: 3};

上面的语句是我们常用的,可是你能解释为什么下面的语句也能正常工作吗?

const [a, b] = 'abc';
const {toString: s} = 123;

任务:

1. 解释上面两个解构语句的工作原理
2. 你能够实现自定义类的数组解构吗?

比如:

class A = …
const a = new A();
const [e1, e2] = a;  //  怎么样才能让自定义的类也能支持支持数组的解构呢?

应用

默认值

ES5时,处理默认值的惯用法:

const scale = opts.scale || 1;

现在可以:

const {scale = 1} = opts;

不过两者并不等价,默认值只会在目标值是undefined时才会生效。

const {scale = 1} = {}
// scale === 1

const {scale = 1} = {scale: null}
// scale === null

const {scale = 1} = {scale: undefined}
// scale === 1

交换

有个技巧是可以在一条语句内实现变量值的交换。

原来需要三句话:

var tmp = a;
var a = b;
var b = a;

现在只需要:

const [a, b] = [b, a];

这让我们在实现一些基础算法时更加精练。

函数返回多个值

在ES5中函数只能返回一个值,有了解构赋值,可以模拟出多个返回值。

const [a, b] = f();

当然从设计上说,js的函数的返回值还是应该是单一的模型比较合适。

很小看到接口层面返回一个数组作为多个值。 可能在编写一些专业领域或DSL应用时会用得到。

而在实现时经常会使用解构赋值带来的便利:

const {name, age} = getInfo();

忽略数组中的一些值

const [a, , c] = [1, 2, 3];
// a === 1
// c === c

我想到的一个应用是一下子从正则表达式match对象中取出多个元素。

const re = /^([^=]+)=(.*)$/;
const [, key, value] = re.exec('name=helijia');
// key === 'name'
// value === 'helijia'

spread

解构赋值运算符配合spread会比较有用。

const {name, age, ...exts} = item;

return <Item {...exts} />;

exts对象中并不包含name和age,如果在ES5中要费好几句语句。

数组也支持spread,但是数组本身具有slice等函数,所以一般用不上。

var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]

重命名

对象的解构还支持重新命名,这在名字冲突,或者简化代码时会比较有用。

const item = {
  artisanNick: '玉米'
  artisanLevel: 10,
  artisanType: 3
};

const {artisanNick:nick, artisanLevel:level, artisanType:type} = item;

原来我们写成

const nick = item.artisanNick;
const level = item.artisanLevel;
const type = item.artisanType;

可配合默认值一起用:

const {a:aa = 10, b:bb = 5} = {a: 3}

函数参数的解构

这在实际开发中就用的比较多了,比如在React组件开发中:

const Product = ({name, price}) => (
  <div>
    <div>name: {name}</div>
    <div>price: {price}</div>
  </div>
);

babel对解构赋值的支持

以上描述的特性使用babel编译器就能在主流浏览器中工作,babel对ES6的支持是通过将代码编译成ES5代码来实现的;
而nodejs和chrome原生是直接支持es6的,它们是基于V8引擎在解释器层面支持ES6,因此两者能力是有差异的。

通过babel编译的ES6,最后本质是ES5代码,是静态的,所以只能支持一些语法糖的功能;

下面是一些示例:

常量

// ES6
const [a, b, c, d] = [1, 2, 3, 4];

// 对应的ES5
var a = 1;
var b = 2;
var c = 3;
var d = 4;

数组

// ES6
const list = [1, 2, 3, 4];
const [a, b, c, d] = list;

// ES5
var list = [1, 2, 3, 4];
var a = list[0];
var b = list[1];
var c = list[2];
var d = list[3];

别名和默认值

// ES6
const {a:aa = 10, b:bb = 5} = {a: 3}

// ES5
var _a = { a: 3 };
var _a$a = _a.a;
var aa = _a$a === undefined ? 10 : _a$a;
var _a$b = _a.b;
var bb = _a$b === undefined ? 5 : _a$b;

重命名和默认值的处理。

字符串

// ES6
const [a, b, c] = '1234';

// ES5
var _ = '1234';
var a = _[0];
var b = _[1];
var c = _[2];

字符串也当成数组一样处理了,所以刚好正常工作。

迭代器

其实只要实现迭代器接口,就能够解构。

const set = new Set([1, 2, 3, 4])
const [a, b, c] = set;
console.log(a, b, c);
// 1 2 3

这段代码在chrome的cosnole和nodejs中都能正常工作,不过在babel中就歇菜了。

因为它编译后的结果为:

var set = new Set([1, 2, 3, 4]);
var a = set[0];
var b = set[1];
var c = set[2];

console.log(a, b, c);
// undefined undefined undefined

当然Map也是实现了迭代器接口的。

const map = new Map();
map.set('window', window);
map.set('document', document);

for (const [key, value] of map) {
  console.log(key + " is " + value);
}

const [[k1, v1], [k2, v2]] = map;   // destructring

再来一个例子:

function* iter() {
  yield 1;
  yield 2;
  yield 3;
}

const [a, b, c] = iter();
console.log(a, b, c);
// 1 2 3

同样这段代码在babel中也不能正常工作。

回到任务

const [a, b] = 'abc';
const {toString: s} = 123;

任务:

1. 解释上面两个解构语句的工作原理
2. 你能够实现自定义类的数组解构吗?

所以以上两个语句能正常工作,原因是分场景的,在通过babel编译成ES5和通过解释器直接执行原理是不一样的。

babel编译器会把它编译成

// ES6
const [a, b, c] = '1234';
const {toString: s} = "123";

// ES5
var _ = '1234';
var a = _[0];
var b = _[1];
var c = _[2];
var _2 = "123";
var s = _2.toString;

而js引擎执行ES6是因为字符串实现了迭代器接口,以及支持对象属性访问。

对于第2个问题,我们可以让自定义类实现迭代器接口来支持,只是在babel中不能正常工作。

以下是一个示例:

class Random {
  [Symbol.iterator]() {
    return {
      next() {
        return {value: Math.random(), done: false};
      }
    }
  }
}

const random = new Random();
for (const n of random) {
  if (n > 0.8) {
    break;
  }
  console.log(n);
}

const [e1, e2, e3, e4] = new Random();
console.log(e1, e2, e3, e4);

规范和V8对解构赋值的支持

运行语义

看到数组的解构处理,第一步总是取得一个迭代器,然后操作这个迭代器。

从规范中知道,解构赋值操作符对应的元素就是 DestructuringAssignment,查询V8代码可知,

V8在parser阶段就会把解构赋值语句重写成等效的赋值语句, 这样解释器不需要做修改就可以运行新的语法,也保证了效率。

关键代码片段:

Pattern Match

使用了近一年半的Elixir,有许多语言特性另人着迷,其中模式匹配就是一个。

在ES6中引入了和模式匹配语法有点接近的解构赋值(Destructring Assigmnent)语句,但是仅仅是部分精简代码的语法糖,而在语义和表达上并没有本质变化。

不过搜索github上,看到已有相关的proposal,以及babel实现的issue,所以借此机会熟悉了解一番。

另外发现一个js库js-pattern-matching,提供了一个函数来支持模式匹配

其中这个JS库在不引入新语法特性的基础上支持较好的模式匹配语法,我觉得挺赞的。 它的原理是利用function.toString,得到函数字符串,再生成匹配的新的函数。

我写了几个简单的示例试用了一下,感觉还不错,不过在类型匹配时有些BUG。

基于解构赋值的匹配

const match = require('js-pattern-matching');

const sum = (list) => match(list) (
  ([x,...xs]) => x + sum(xs),
  ([]) => 0
);

console.log(sum([]));
console.log(sum([1, 2, 3, 4]));

常量匹配

因为要符合语法,所以加个前续v=,文档说是可改成其他字母。

const fibs = (n) => match(n) (
  (v= 0) => 0,
  (v= 1) => 1,
  _ => fibs(n - 2) + fibs(n - 1)
);

for (let i = 0; i < 10; i++) {
  console.log(fibs(i));
}

类型匹配

const type = (v) => match(v) (
  (Array) => 'array',
  (Object) => 'object',
  _ => 'unknow'
);

我也看了proposal的语法,感觉风格和原来的js差异太大,设计成Expression,可以在任何地方使用,可能会因为功能太强而导致滥用,反而起不到原来模式匹配优雅简洁的目的。

其他人的一些探索,不过这个语法不是很美观。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant