ES6 快速上手:Symbol 类型

es6-small

ES5 的时候有六种类型:

  • undefined
  • null
  • Boolean
  • Number
  • String
  • Object

现在,ES6 增加了一种新类型:Symbol。下面是一个创建 symbol 的例子:

var mySymbol = Symbol();
typeof mySymbol; //"symbol"

调用 Symbol 函数就可以创建一个 symbol。因为 symbol 是一个原始类型,并不是对象,所以 new Symbol() 会报错。

symbol 的用处就是作为对象的键。用 symbol 作为对象的键可以避免传统的属性名冲突。比如,用上例的 mySymbol 作为键:

obj[mySymbol] = "ok!"; 

创建 symbol 时,可以传入一个字符串描述:

var sym = Symbol("hi");

即使你每次都传入相同的字符串,返回的 symbol 也互不相等。正因为有这个特性,所以 Symbol 类型可以很好地解决属性名冲突的问题。

什么是冲突呢?当多人合作编码的时候,经常会出现你往对象上加了一个某某属性(比如 $),他人正好也想到了这个名称,当你们同时用了这个名称作为属性,代码之间就会发生冲突,互相覆盖。而用 symbol,即使都用了相同的描述,也不是同一个 symbol。

再列出 Symbol 类型的一些特点:第一,以 symbol 为键的属性,不能用 obj.prop 形式访问,只能用方括号。

第二,for...in 循环、Object.keysObject.getOwnPropertyNames 在遍历一个对象时,都会忽略 symbol 键。

如果你想查看对象上有哪些 symbol 键,可以使用 ES6 的新 API:Object.getOwnPropertySymbols(),返回一个数组,里面都是对象所包含的 symbol。

如果想查看所有属性,用另一个 API:Reflect.ownKeys(obj),会同时返回字符串键和 symbol 键。

第三,in 操作符可以正常工作,比如上例,mySymbol in obj会返回 true。执行 delete obj[mySymbol] 也可以删除这个属性。

第四,symbol 类型不能被自动转换为字符串:

var sym = Symbol("hello");
alert("my symbol is " + sym); // TypeError

可以通过 String(sym) 或调用 sym.toString()

共享 symbol

由于每次调用 Symbol("hello") 返回的 symbol 都是不同的,但有时会有共享同一个 symbol 的需要,所以 ES6 提供了一个全局的 symbol 注册表。

如果你想要共享一个 symbol,用 Symbol.for() 来代替 Symbol()

let sym = Symbol.for("hello");

调用 Symbol.for() 时,如果传入相同的字符串,那么返回的就是同一个 symbol。如果该 symbol 还不存在,则会创建一个 symbol 返回。

Symbol.for() 创建的 symbol 上,还可以调用 Symbol.keyFor() 来查看它的描述。

let sym = Symbol.for("hello");
let sym2 = Symbol.for("hello");
Symbol.keyFor(sym); //"hello"
Symbol.keyFor(sym2); //"hello"

let sym3 = Symbol("hello");
Symbol.keyFor(sym3); // undefined

你可以看到在一个普通 Symbol() 创建的 symbol 上调用 Symbol.keyFor() 会发生什么。

内置 symbol

ES6 内置了一些 symbol,它们都作为 Symbol 函数的属性存在,比如后面关于迭代器文章里要提到的 Symbol.iterator。但是,调用 Object.getOwnPropertySymbols() 时,这些内置 symbol 不被计算在内。

ES6 快速上手:类

es6-small

ES6 终于引入了类。下面是一个类的例子:

class Person {
  constructor(age, country) {
    this._age = age;
    this._country = country;
  }

  get age() {
    return this._age;
  }

  get country() {
    return this._country;
  }

  toString() {
    return `${this.age} ${this.country}`;
  }
}

在上例中,constructor 代表类的构造函数。constructor 里面定义的属性,都是实例属性。定义类时,可以没有 constructor

agecountrytoString 都是原型属性和方法,相当于定义在了 Person.prototype 上。其中 agecountry 是只读的。

如你所见,属性和属性之间,不再需要分隔符了。你可以继续用逗号,或者用分号也可以。

类的语法

第一种是类的声明:

class MyClass {
 ...
}

语法要点:

  • 注意名字后面不能有括号。
  • 类的声明不会被提升到作用域顶部,所以必须先定义后使用。
  • classlet 一样,不能重复声明。

第二种是类表达式:

var MyClass = class {
  constructor() {
    //一个实例属性
    this.name = "demo";
  }
  method() {
    //一个原型方法
  }
}

这种语法下,class 后面也可以再加一个标识符:

var MyClass = class SameClass {
  ...
}

此时 MyClassSameClass 是一样的,可以互相替换。

创建实例还和以前一样:

var instance = new MyClass();

类的一些特点

如果你用 typeof MyClass,会发现其实它还是一个 function

如果没有 new,直接调用 MyClass 会抛出错误。

声明类的代码会全部在严格模式下执行,相当于自动加了 "use strict";

class 里面定义的所有方法都是不可枚举的。也就是说,不能通过 for...in 遍历到,Object.keys 也获取不到。

类也可以当作函数的参数:

function createObj(MyClass) {
  return new MyClass();
}

let obj = createObj(class {
  foo() {
    console.log("hello");
  }
});
obj.foo(); //"hello"

类也可以立即执行,只要在后面多加个括号:

let singleton = new class {
  constructor(foo) {
    this.foo = foo;
  }
  bar() {
    ...
  }
}("hello");

可以用来创建单例(singleton)。

访问器属性

类也可以添加访问器属性,像对象一样。访问器属性就是一对 getter/setter,语法如下:

class Person {
  constructor(name) {
    this.name = name;
  }

  get age() {
    return this.age;
  }
  set age(value) {
    this.age = value;
  }
}

如果省略 setter,属性就是只读的。

静态成员

class Person {
  static walk(a, b) {
     ...
  }
}

静态方法的调用:Person.walk(),其实等同于在 Person 上创建了 walk() 方法。

静态成员默认也是不可枚举的。

阅读全文 »

ES6 快速上手:解构赋值

es6-small

这个特性又叫做“destructuring”。其语法的一般形式为:

[ variable1, variable2, ..., variableN ] = array;

这是数组的解构赋值。后文还有对象的解构赋值。

假设有数组 value = [1, 2, 3],要把三个值分别赋给三个变量 a, b, c,在ES5中要这样写:

var a = value[0];
var b = value[1];
var c = value[0];

ES6 中使用解构赋值:

[a, b, c] = value;

如果需要同时声明变量:

let [a, b, c] = value;

当然换成 var, const 也同样可以。结合“rest”使用:

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

也可以嵌套:

var value = [1, 2, [3, 4, 5]];
var [a, b, [c, d]] = value; //a,b,c,d分别为1,2,3,4

在指定位置省略:

var value = [1, 2, 3, 4, 5];
var [a, , c, , e] = value; //1,3,5

如果出现越界访问的情况,值是 undefined

var value = [1];
var [a, b] = value; //1,undefined

这时候可以指定默认值:

var value = [1];
var [a = 100, b = 2] = value; //1,2

注意的是默认值只对 undefined 值起作用:

var value = [null, null];
var [a = 1, b = 2] = value; //a,b还是null

用处:可以用来交换两个变量的值(没有比这更简单的了):

[el1, el2] = [el2, el1];

或者,从正则表达式的结果里取出分组:

var [, firstName, lastName] = "John Doe".match(/^(w+) (w+)$/);

对象的解构赋值:

var personA = { name: "Paul" };
var personB = { name: "Lily" };

var { name: nameA } = personA;
var { name: nameB } = personB;

console.log(nameA); // "Paul"
console.log(nameB); // "Lily"

对象的 name 属性分别赋给了 nameAnameB。如果用下面这种简洁的写法:

var personA = { name: "Paul" };
var { name } = personA;

那么会把 name 属性赋给同名的变量 name

深层嵌套的对象:

var person = {
  name: {
    firstName: "John", 
    lastName: "Doe"
  }
};
var {name: {firstName, lastName}} = person; //得到两个变量firstName,lastName,注意name未定义

对象里嵌套数组:

var person = {
  dateOfBirth: [1, 1, 1980]
};
var {dateOfBirth: [day, month, year]} = person; //得到三个变量day, month, year

发生嵌套时,一般后面跟了冒号的变量都是未定义的,如上例的 dateOfBirth。这点要注意。

如果要解构的属性不存在,得到的是 undefined。对象解构赋值也可以使用默认值:

var {firstName = "John", lastName: userLastName = "Doe"} = {};

默认值对于 null 同样不起作用。

注意对于下面的写法,会抛出错误:

{ blowUp } = { blowUp: 10 }; // SyntaxError 

因为JS中任何以 { 开始的语句都被解析为一个块语句。解决方案是将整个表达式用一对小括号包裹:

({ blowUp } = { blowUp: 10 });

用处:第一,函数参数的解构:

function myFunc(options) {
  if (options.a) ...
  if (options.b) ...
}

这种语句经常出现在我们的代码中。用 ES6 就看上去更简洁:

function myFunc( {a, b} ) {
  if (a) ...
  if (b) ...
}

阅读全文 »

ES6 快速上手:模板字符串

es6-small

ES6 引入了一种新型的字符串字面量语法,称为模板字符串(template strings)。使用反撇号(`)来代替普通字符串的引号。下面是一个例子:

let greeting = `Hello Jackie`!

有什么用呢?模板字符串为 JavaScript 提供了简单的插值功能(string interpolation)。例:

let name = "Jackie";
let greeting = `Hello ${name}`!

${name} 称为占位符(placeholder)。模板占位符可以是任意 JavaScript 表达式,比如函数调用、算数运算等,都可以作为占位符使用,甚至可以嵌套另一个模板字符串。

如果占位符计算出的值不是字符串,那么会调用 toString 方法。

如果要书写反撇号(`),或者 $,或者 {,都需要转义(加反斜杠)。

与普通字符串的一大区别是,模板字符串可以多行书写:

$("#warning").html(`
  <h1>你好!</h1>
  <p>我叫${name}。这是一个多行
  模板字符串的
  示例。</p>
`);

所有的空格、新行、缩进,都会原样输出在生成的字符串中。

但是,模板字符串并不会自动转义 HTML 字符(比如 < > 等),而且,不支持条件语句、遍历,比其他的模板库还是弱了些。为了突破这些限制,ES6 提供了标签模板(tagged templates)。只需要在起始的反撇号前加上一个额外的标签,如下面的 htmlspecial

var message = htmlspecial`<p>${htmlStr}</p>`;

上面的代码等效于

var message = htmlspecial(templateData, htmlStr);

templateData 是一个不可变数组,存储着模板所有的字符串部分,也就是除去占位符以后剩下的部分,比如上例中 ${htmlStr} 将模板分割为两部分,所以 templateData 数组内就是两个元素,形如 ["<p>", "</p>"]。同时这个数组是不能更改的,相当于用了 Object.freeze

htmlspecial 的一种实现(用来将 < > 等特殊字符转义为实体引用):

function htmlspecial(templateData) {
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
    var arg = String(arguments[i]);

    // 转义HTML特殊字符。
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // 拼接
    s += templateData[i];
  }
  return s;
}

函数返回的值就会当作模板字符串的输出。

ES6 快速上手:箭头函数

es6-small

ES6 添加了新的函数语法:箭头函数。语法像下面这样:

([param, param, ...]) => {
  函数体
}

几个例子:

() => { ... } //无参数
x => { ... } //一个参数
(x, y) => { ... } //多个参数
(x, y) => {
  if(x > 1) {
    ...
  }
}

可以这样用:

let foo = x => { return x + 10; }

等同于下面:

function foo(x) {
  return x + 10;
}

如果只有一个 return 语句的话,还可以简写成:

let foo = x => x + 10; // x + 10的值会被返回

当然,如果简写的话你需要注意,如果返回的是一个对象,比如一个空对象:

let foo = x => {}; //报错

这时需要外加一层小括号:

let foo = x => ({}); //OK
//或者老老实实写成:
let foo = x => {
  return {};
};

立即执行的函数(IIFE)在 ES5 中是这样:

(function(name) {
  var greetings = "Hello " + name;
  return greetings;
})("Ethan");

同样可以换成箭头函数:

(name => {
  let greeting = "Hello " + name;
  return greeting;
})("Ethan");

箭头函数有几个特性,首先,是 this 值固定。this 值取决于箭头函数定义时的上下文,与如何调用无关:

var person = {
  name: "Bob",
  sayHello: () => "Hello " + this.name;
};

var hello = person.sayHello;
hello(); //"Hello Bob"

并且,this 的值是无法改变的。一旦创建,this 值就已绑定。

第二,箭头函数不能用作构造函数,当和 new 一起使用时会报错。

第三,箭头函数没有自己的 arguments 对象。所以,在箭头函数内访问 arguments,这时访问的其实是外层函数的 arguments 对象。你可以使用 ES6 的新特性来代替 arguments

let foo = (...args) => {
  console.log(args); //和arguments一样
};

最后,箭头函数和普通函数一样,也会在脚本执行前被提升到作用域顶部。

ES6 快速上手:初探

es6-small

最新版的 JavaScript 标准已经发布了,正式的名称叫做 ECMAScript 2015,也就是我们常说的 ES6,或者还有个更老的名字叫 ES Harmony。因为制订标准的是 ECMA 组织所以叫做 ECMAScript 。 JavaScript 其实就是对 ECMAScript 的实现,或者说是 ECMAScript 的一种方言。

所以,前端领域的新知识真是源源不断,层出不穷……时时刻刻都不能停止学习啊。准备写成一个系列,这些内容都是我自己整理的。

第一篇还是先讲一些比较简短的特性吧。

参数默认值

在 ES6 中,函数参数可以设置默认值了。上例子:

function increment(n = 1) {
  return n + 100;
}

在以前我们需要这样:

function increment(n) {
  n = n || 1; // foo的默认值设为1
  return n + 100;
}

如果没有传入 n,那么 n 的值就是 1。注意,如果你传入 undefinedn 的值还会是 1

可以只给某一个参数设置默认值:

function sum(a, b = 2, c) {
  return a + b + c;
}

可以执行函数来得到默认值:

function foo( bar = getBar() ) {
  return foo + 10;
}

剩余参数

这个特性也叫做“rest”,操作符是三个点 ...,用来包含剩余参数。例子:

function foo(x, ...y) {
  console.log(y);
}
foo(1, "hello", true); // ["hello", true]

可以看到,除了 x 之外剩下的所有参数都被放入数组中传给了 y,这就是“rest”的作用。

y 总是一个数组,里面包含了剩余的所有参数。上例中,如果只传入一个参数,那么 y 就是空数组。

注意这个特性只能用在最后一个参数上,下面是错误的:

// 错误
function foo(x, ...y, z) {
  console.log(y);
}

数组拆分

又叫“spread”,也是三个点,但是用在数组之前,例如 ...[a, b, c]

作用是可以把数组拆分成 a, b, c 的形式。拆分后,有两个用处:第一,是给函数传参:

function sum(a, b, c) {
  return a + b + c;
}
sum(...[1, 2, 3]); // 等于sum(1,2,3)

数组被拆成了三个参数传入了 sum 函数。在 ES5 中,我们就需要这样写:

sum.apply(undefined, [1, 2, 3]);

第二个用处,是用在数组字面量里:

var foo = ['b', 'c'];
var combined = ['a', ...foo]; // 等于['a', 'b', 'c']

对象字面量扩展

ES6 还对对象字面量进行了一些扩展,比如一些简写,下面是简写前的代码:

var obj = {
  foo: foo,
  toString: function() {
    return this.foo;
  }
}

使用 ES6 可以简写成为:

var obj = {
  // 两个foo简写成一个foo
  foo,
  // 省略function关键字
  toString() {
    return this.foo;
  }
}

还有一些扩展:

var obj = {
  //设置原型对象
  __proto__: theProtoObj,

  //属性名可以动态计算
  [ "prop_" + 42 ]: 42
}

动态计算的属性名,使用中括号,里面可以放入任何表达式,包括调用函数。

let

JavaScript 一直都没有局部作用域,这是跟其他语言相比一个奇葩的地方。比如,想要在条件语句中声明变量:

if(isBlack) {
  var black = true;
} else {
  var white = true;
}

你希望在 isBlack=true 时才定义变量 black,否则就定义一个变量 white。结果在 JavaScript 中并不是这样,两个变量都会被定义。只不过其中一个是 undefined

现在 ES6 中引入了 let 以后就不一样了。let 的作用和 var 一样,都是用来定义变量,但一个重要区别是,let 定义的变量只在局部作用域中有效。比如下面:

if(a === 1) {
  let b = 2;
}

for(let c = 0; c < 3; c++) {
  ...
}

function foo() {
  let d = 4;
}

b, c, d 变量都只在相应的代码块中有效,在其他地方,这些变量都是未声明的。如果访问,就抛出 ReferenceError。也就是变量的有效范围仅限于花括号内。当然了,也包括 for 后面的圆括号。
阅读全文 »

flight