ES6 特性总结

ES6 包装了许多语法糖,使用 ES6 编写的代码相比 ES5 来说要更加的简洁清晰。

得益于 babel ,我们在日常开发时可以比较自由的使用 ES6、ES7。

下面开始总结介绍 ES6 特性。

1. let 与 const

除了 var ,我们现在还可以使用两个新的标示符来定义一个变量 —— letconst。和 var 不一样的是,letconst 不存在变量提升。

使用var#1处会产生变量提升,使用 ES5 时我们可能会无意屏蔽到外层作用域变量:

var snack = 'Meow Mix';

function getFood(food) {
    if (food) {
        var snack = 'Friskies'; // #1
        return snack;
    }
    return snack;
}

getFood(false); // undefined

而通过使用let,我们可以避免出现这种现象:

let snack = 'Meow Mix';

function getFood(food) {
    if (food) {
        let snack = 'Friskies';
        return snack;
    }
    return snack;
}

getFood(false); // 'Meow Mix'

原因是因为,letconst会产生一个块级作用域,因此在变量未被定义之前使用它会产生一个 ReferenceError

console.log(x); // ReferenceError: x is not defined
let x = 'hi';

要注意的是,这里还有一个概念“暂时性死区”:在代码块内使用let命令声明变量之前,该变量都是不可用的,不受外部变量影响。

let x = 'hi';

function sayHi() {
    console.log(x);
    let x = 'hii';
    console.log(x);
}

sayHi(); // Uncaught ReferenceError: x is not defined (Line 4);

所以,虽然let不会发生变量提升,但是会出现暂时性死区,这个是需要注意的。

另外,letconst在同一作用域层级中是不允许重复声明的。

对于letconst特性的总结如下:

  1. 块局作用域;
  2. 不存在变量提升,一定声明后才能使用;
  3. 暂时性死区,在代码块内使用let命令声明变量之前,该变量都是不可用的,不受外部变量影响;
  4. 在相同作用域范围内不允许重复声明;
  5. constlet不同点在于:
    • const如果声明的变量是简单的值,则不能改变变量的值,修改会报错;
    • const如果声明的是复合类型的变量,则只保证变量地址不变,值可以变;

2. 块级作用域 { }

在 ES6 以前,我们经常需要使用函数立即执行表达式(IIFE)来创造一个内部作用域。

例如:

(function () {
    var food = 'Meow Mix';
}());

console.log(food); // Reference Error

而在 ES6 中,我们已经可以直接使用块级作用域了。

{
    let food = 'Meow Mix';
    console.log('[Block Scope] ', food); // '[Block Scope] Meow Mix'
}

console.log(food); // Reference Error

3. 箭头函数

我们经常需要给回调函数维护一个词法作用域的上下文 this

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character; // Cannot read property 'name' of undefined
    });
};

常用的解决办法是通过闭包变量寄存this

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    var that = this; // Store the context of this
    return arr.map(function (character) {
        return that.name + character;
    });
};

或者, 通过传递this

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character;
    }, this);
}

还可以通过上下文绑定的方式维护this

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character;
    }.bind(this));
};

箭头函数的出现,其中一个好处就是可以消除这小部分维护this的冗余:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(character => this.name + character);
};

使用箭头函数时,this会自动绑定到当前作用域的this

箭头函数还有另一个好处,可以更加简洁并很简单地返回一个值

函数式编程注重数据到数据之间的映射,而箭头函数的出现某种程度上就是函数式的一种体现。

const arr = [1, 2, 3, 4, 5];
const squares = arr.map(x => x * x);
console.log(squares); // [1, 4, 9, 16, 25];

4. 一些字符串工具函数

在 ES6 中,标准库升级了很多,在这些变化中有许多新的对于字符串的函数,比如 .includes().repeat()

4.1 .includes()

在 ES5 中我们使用 indexOf() 函数的返回值是否 >-1 来判断字符串是否包含某些字符串:

var string = 'food';
var substring = 'foo';
console.log(~string.indexOf(substring));

而现在现在我们更简单地使用 .includes() 来返回一个布尔值来判断:

 const string = 'food';
const substring = 'foo';

console.log(string.includes(substring)); // true

4.2 .repeat()

在 ES5 中,我们需要实现将一个字符串重复 n 次进行拼接:

function repeat(string, count) {
    var strings = [];
    while(strings.length < count) {
        strings.push(string);
    }
    return strings.join('');
}

在 ES6 中,可以更简便地实现:

'meow'.repeat(3); // 'meowmeowmeow'

4.3 模板字符串 ` `

模板字符串的出现,提供了很多便利。

4.3.1 免转义

使用模版字符串我们可以不用对某些特殊字符进行转义处理。

ES5:

var text = "This string contains \"double quotes\" which are escaped.";

ES6:

let text = `This string contains "double quotes" which don't need to be escaped anymore.`;

4.3.2 字符串插值

模版字符串还支持字符串插值,可以把变量值(或表达式)和字符串连接起来。

ES5:

var name = 'Tiger';
var age = 13;

console.log('My cat is named ' + name + ' and is ' + age + ' years old.');

ES6:

const name = 'Tiger';
const age = 13;

console.log(`My cat is named ${name} and is ${age} years old.`);

4.3.3 换行

在 ES5 中,需要换行时,需要这样:

var text = (
    'cat\n' +
    'dog\n' +
    'nickelodeon'
);

或者这样:

var text = [
    'cat',
    'dog',
    'nickelodeon'
].join('\n');

模板字符串可以支持换行并且不需要进行额外的处理:

let text = ( `cat
dog
nickelodeon`
);

不过使用模板字符串时需要注意,需要使用括号将表达式括起来或者在末尾加上分号。例如:

// bad
let text = `cat`

// good
let text1 = `cat`;
let text2 = (`cat`)
let text3 = (`cat`);

为什么需要这样呢?

因为我们平常写 ES6 时都是需要经过 babel 转码的,而如果模板字符串末尾省略分号,而下一行紧接着是一个 IIFE 的话,那么这个字符串如cat会被当成一个函数调用。例如:

let text1 = `cat`
(function() { // todo })();

转码后:

var text1 = cat(function() { ... })(); // cat is not a function

某天刷知乎偶然看到这个坑,有关这个坑的具体介绍可阅读这篇文章:不写分号的前端攻城师们注意了!需要加分号的情况又多了一种

5. 解构赋值

解构赋值可以让我们用一个更简便的语法从一个数组或者对象(即使是深层的)中分离出来值,并存储他们。

5.1 解构数组

let [a, b, c, d] = [1, 2, 3, 4];

console.log(a); // 1
console.log(b); // 2

5.2 解构对象

const luke = { occupation: 'jedi', father: 'anakin' };
const  {occupation, father } = luke;

console.log(occupation); // 'jedi'
console.log(father); // 'anakin'

6. 模块

在 ES6 之前,我们需要使用类似 Browserify的库来创建客户端的模块化,在 node.js 中需要使用 require

而在 ES6 中,我们可以同时使用 AMD 和 CommonJS 类型的模块化。

6.1 导出

6.1.1 使用 CommonJS 出口

module.exports = 1;
module.exports = { foo: 'bar' };
module.exports = ['foo', 'bar'];
module.exports = function bar () {};

6.1.2 使用 ES6 的出口

在 ES6 中我们可以暴露多个值。

export let name = 'Gison';
export let age  =21;​​

或者暴露一个对象列表:

function sumTwo(a, b) {
    return a + b;
}

function sumThree(a, b, c) {
    return a + b + c;
}

export { sumTwo, sumThree };

我们还可以暴露函数、对象和其他的值,通过简单地使用 export 这个关键字:

export function sumTwo(a, b) {
    return a + b;
}

export function sumThree(a, b, c) {
    return a + b + c;
}

最后,我们还可以绑定一个默认的输出:

function sumTwo(a, b) {
    return a + b;
}

function sumThree(a, b, c) {
    return a + b + c;
}

let api = {
    sumTwo,
    sumThree
};

export default api;

/* Which is the same as
 * export { api as default };
 */

6.2 导入

在 ES6 中同样提供了多样的导入方式。

6.2.1 整体导入

import 'underscore';

6.2.2 解构导入

import { sumTwo, sumThree } from 'math/addition';

6.2.3 重命名导入

import {
    sumTwo as addTwoNumbers,
    sumThree as sumThreeNumbers
} from 'math/addition';
import * as util from 'math/addition';

6.2.4 混合导入

export { foo as default, foo1, foo2 }
import foo, { foo1, foo2 } from 'foos';

import React, { Component, PropTypes } from 'react'; 

7. 参数

在 ES5 中,在函数中我们需要各种操作去处理默认参数不定参数重命名参数等需求,在 ES6 中我们可以直接使用更简洁的语法完成这些需求。

7.1 默认参数

ES5:

function addTwoNumbers(x, y) {
    x = x || 0;
    y = y || 0;
    return x + y;
}

ES6 中,函数的参数可以支持设置默认值::

function addTwoNumbers(x=0, y=0) {
    return x + y;
}
addTwoNumbers(2, 4); // 6
addTwoNumbers(2); // 2
addTwoNumbers(); // 0

7.2 rest 参数

在 ES5 中,我们需要这么处理不定参数:

function logArguments() {
    for (var i=0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}

在 ES6 中,使用 rest ,我们可以这样处理不确定数量的参数:

function logArguments(...args) {
    for (let arg of args) {
        console.log(arg);
    }
}

也可以使用 rest 表达式进行展开操作。 在 ES5 中,我们可以 apply Math.max 方法来获得一个数组中的最大值:

Math.max.apply(null, [-1, 100, 9001, -32]); // 9001

在 ES6 中,我们可以直接通过展开操作把一个数组的值作为参数传递给一个函数:

Math.max(...[-1, 100, 9001, -32]); // 9001

我们可以更简洁地使用这个语法来合并数组:

let cities = ['San Francisco', 'Los Angeles'];
let places = ['Miami', ...cities, 'Chicago']; // ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']

7.3 命名参数

在 ES5 中使用配置对象的模式来进行命名参数或参数纠正,例如:

function initializeCanvas(options) {
    var height = options.height || 600;
    var width  = options.width  || 400;
    var lineStroke = options.lineStroke || 'black';
}

在 ES6 中,我们可以直接利用解构一个函数的形参实现相同的功能:

function initializeCanvas({ height=600, width=400, lineStroke='black' }) {
    // Use variables height, width, lineStroke here
 }

如果我们想使整个值可选择,我们可以结合默认参数:

function initializeCanvas({ height=600, width=400, lineStroke='black' } = {}) {
    // ...
}

8. 类 Class

在 ES6 之前,我们通过构造函数来创造一个类,并且通过原型来扩展属性:

function Person(name, age, gender) {
    this.name   = name;
    this.age    = age;
    this.gender = gender;
}

Person.prototype.incrementAge = function () {
    return this.age += 1;
};

然后可以这样继承类:

function Personal(name, age, gender, occupation, hobby) {
    Person.call(this, name, age, gender);
    this.occupation = occupation;
    this.hobby = hobby;
}

Personal.prototype = Object.create(Person.prototype);
Personal.prototype.constructor = Personal;
Personal.prototype.incrementAge = function () {
    Person.prototype.incrementAge.call(this);
    this.age += 20;
    console.log(this.age);
};

var Gison = new Personal('Gison', 21, '本科', 'FE', 'coding');

在 ES6 中,提供了更多的语法糖,可以直接创造一个类:

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

    incrementAge() {
      this.age += 1;
    }
}

使用 extends 关键字来继承一个类:

class Personal extends Person {
    constructor(name, age, gender, occupation, hobby) {
        super(name, age, gender);
        this.occupation = occupation;
        this.hobby = hobby;
    }

    incrementAge() {
        super.incrementAge();
        this.age += 20;
        console.log(this.age);
    }
}

当然,ES6 中虽然实现了“类”,但 JavaScript 中实际上是没有像 Java 那样的类存在的。JavaScript 中的类的实现同样是基于原型链的。

9. Symbol

在 ES6 之前 只有 6 种基本数据类型,包括 5 种基本数据类型(UndefinedNulllBooleanNumberString)和 1 种引用数据类型(Object)。在 ES6 中又新增了一种基本数据类型,**Symbol``,它表示独一无二的值。

9.1 生成 Symbol

方法 1

let s = Symbol();
console.log(s); // Symbol()
typeof s; // "symbol"

方法 2:我们还可以给Symbol函数传递一个参数,用于对这个 Symbol 值进行描述,方便我们区分不同的 Symbol 值,以及调试。

let s1 = Symbol('s1');
let s2 = Symbol('s2');

typeof s1; // "symbol"
typeof s2; // "symbol"

console.log(s1); // Symbol(s1)
console.log(s2); // Symbol(s2)

s1 === s2; // false
s1.toString(); // "Symbol(s1)"
s2.toString(); // "Symbol(s2)"

let s3 = Symbol('s2');
s3 === s2; // false

从上面的代码中,我们可以看出来:

  1. 我们可以通过给 Symbol 函数传递一个参数,用来区别不同的Symbol值。
  2. 给 Symbol 函数传递了描述参数的 Symbo l值不论是通过控制台打印,还是调用它自身的 toString 方法都会显示出来它的描述词,便于区分。
  3. 就算给 Symbol 函数传递相同的描述字符串,它生成的 Symbol 值也是不一样的。只是它们的描述符是一样的。

方法 3:我们还可以使用另一个生成 Symbol 值的函数Symbol.for()来生成 Symbol 类型的值。

// 方法三
let s4 = Symbol.for('s4');
console.log(s4); // Symbol(s4)
s4.toString(); // "Symbol(s4)"

let s5 = Symbol.for('s4');
s5 === s4; // true

Symbol.keyFor(s3); // undefined
Symbol.keyFor(s4); // "s4"

使用Symbol.for(describe)方法会先在全局环境中查找有没有使用 describe 注册的 Symbol 值,如果有的话就会返回找到的那个值,如果没有的话就会重新创建一个。这样做的目的使我们可以重复使用之前定义过的 Symbol 值,或者修改与之相关的一些东西。

说到这里,我们就要提及另一个与Symbol.for()相关的函数Symbol.keyFor(),这个函数是用来查找一个 Symbol 值的注册信息的,如果你是使用Symbol()函数创建的 Symbol 值,不论你传不传递参数,那么通过Symbol.keyFor()函数是查找不到它的注册信息的。

9.2 使用 Symbol

那么 Symbol 有什么使用场景呢?

9.2.1 作为对象的属性名,避免覆盖重写

在实际的工作中,我们可能会经常使用到别人写的类库,然后因为不知道某个对象的属性名,就可能不小心重写了那个对象的某个属性,导致一些不必要的错误出现。但是有了 Symbol 类型后,我们可以很容易的避免掉这样的失误。

// 使用字符串或者 Symbol 值来作为对象的属性了
let a = {};
let b = {};
let c = {};
let s4 = Symbol('s4');

// 第 1 种方法使用 Symbol 作为属性名
a[s4] = 1;
// 第 2 种方法使用 Symbol 作为属性名
b = { [s4]: function(){}}
// 第 3 种方法使用 Symbol 作为属性名
Object.defineProperty(c, s4, {value: 'hello'});

a[s4]; // 1
b[s4]; // function(){}
c[s4]; // "hello"

a.s4; // undefined

使用 Symbo l值作为对象的属性名称可以有效地避免属性名的覆盖或者改写,当然你也要付出一点小代价:使用对象的 Symbol 值作为属性名字的时候,获取相应的属性值需要使用obj[symbol]的方式来获取相应的属性值。不可以使用.运算符来获取相应的属性值

9.2.2 作为一些常量来使用,可以有效地避免覆盖

// 比如一个log()函数,可能有不同级别的输出信息,我们可以定义一组常量来表示
const LOG_LEVEL = {
    NORMAL: Symbol('normal'),
    WARN: Symbol('warn'),
    ERROR: Symbol('error')
}
// log不同级别的信息
log(LOG_LEVEL.NORMAL, '...');
log(LOG_LEVEL.ERROR, '...');

9.3 获取对象上的 Symbol() 值

使用 Symbol 值作为对象的属性名,也需要我们注意一点,那就是我们不能够通过使用Object.keys()Object.getOwnPropertyNames()来获取这些属性名了,当然这些属性名也不会出现在for ...in或者for...of循环中:

// a就是上面的那个对象a,我们给它新增加一个字符串属性
a.name = 'hello';

Object.keys(a); // ["name"]
Object.getOwnPropertyNames(a); // ["name"]
for(var i in a) {console.log(i)} // name
for(var i of a) {console.log(i)} // name

我们可以使用Object.getOwnPropertySymbols()方法获取一个对象上的 Symbol 值的属性名。也可以使用一个新的 API Reflect.ownKeys()函数返回一个对象的所有键名,包括常规的字符串键名以及 Symbol 键名。

Object.getOwnPropertySymbols(a); // [Symbol(s4)]
// 返回所有键名
Reflect.ownKeys(a); // ["name", Symbol(s4)]

10. Map

Map 在 JavaScript 中是一个非常必须的数据结构,在 ES6 之前,我们通过对象来实现哈希表:

var map = new Object();
map[key1] = 'value1';
map[key2] = 'value2';

而 ES6 中的 Map 允许我们对值进行 setgetsearch 的操作:

let map = new Map();
map.set('name', 'Gison');
map.get('name'); // Gison
map.has('name'); // true

Map 令人惊奇的部分就是它不仅限于使用字符串作为 key,还可以用其他任何类型的数据作为 key:

let map = new Map([
    ['name', 'Gison'],
    [true, 'false'],
    [1, 'one'],
    [{}, 'object'],
    [function () {}, 'function']
]);

for (let key of map.keys()) {
    console.log(typeof key); // string, boolean, number, object, function
}

基于这个特性,在我们实现数组去重时,就可以考虑采用 ES6 的 Map 来解决了。

除了.keys()外,我们还可以使用 .entries() 来遍历迭代:

for (let [key, value] of map.entries()) {
    console.log(key, value);
}

WeakMap

在 ES6 之前,为了存储私有变量,我们有各种各样的方法去实现,其中一种方法就是用命名约定:

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

    _incrementAge() {
        this._age += 1;
    }
}

但是命名约定在代码中仍然会令人混淆并且并不会真正的保持私有变量不被访问。现在,我们可以使用 WeakMap 来存储变量:

let _age = new WeakMap();
class Person {
    constructor(age) {
        _age.set(this, age);
    }

    incrementAge() {
        let age = _age.get(this) + 1;
        _age.set(this, age);
        if (age > 50) {
            console.log('Midlife crisis');
        }
    }
}

在 WeakMap 存储变量时,它的 key 不会产生属性名称,可以使用 Reflect.ownKeys() 来查看这一点:

const person = new Person(50);
person.incrementAge(); // 'Midlife crisis'
Reflect.ownKeys(person); // []

实际的实践就是可以用 WeakMap 来储存 DOM 元素,WeakMap 存储变量采用的是弱引用机制,在存储时不会增加变量的引用次数,因而不会污染元素本身:

let map = new WeakMap();
let el  = document.getElementById('someElement');

// Store a weak reference to the element with a key
map.set(el, 'reference');

// Access the value of the element
let value = map.get(el); // 'reference'

// Remove the reference
el.parentNode.removeChild(el);
el = null;

// map is empty, since the element is destroyed

当一个对象被垃圾回收机制销毁的时候, WeakMap 将会自动地移除关于这个对象地键值对。

12. Promise

Promise 可以让我们远离平行的代码(回调地狱):

func1(function (value1) {
    func2(value1, function (value2) {
        func3(value2, function (value3) {
            func4(value3, function (value4) {
                func5(value4, function (value5) {
                    // Do something with value 5
                });
            });
        });
    });
});

转变成垂直代码:

func1(value1)
    .then(func2)
    .then(func3)
    .then(func4)
    .then(func5, value5 => {
        // Do something with value 5
    });

Promise 的好处:对错误的处理使用一些列回调会使代码很混乱,使用 Promise ,我们可以清晰地让错误冒泡并且在合适的时候处理它,而且,在 Promise 确定了 resolved/rejected 之后,他的值是不可再次改变的。

实际例子:

var request = require('request');

return new Promise((resolve, reject) => {
  request.get(url, (error, response, body) => {
    if (body) {
        resolve(JSON.parse(body));
      } else {
        resolve({});
      }
  });
});

我们还可以使用 Promise.all() 来并行处理多个异步函数:

let urls = [
  '/api/commits',
  '/api/issues/opened',
  '/api/issues/assigned',
  '/api/issues/completed',
  '/api/issues/comments',
  '/api/pullrequests'
];

let promises = urls.map((url) => {
  return new Promise((resolve, reject) => {
    $.ajax({ url: url })
      .done((data) => {
        resolve(data);
      });
  });
});

Promise.all(promises)
  .then((results) => {
    // Do something with results of all our promises
 });

13. Generator 生成器

就像 Promise 可以帮我们避免回调地狱,Generator 可以帮助我们让代码风格更整洁——用同步的代码风格来写异步代码,它本质上是一个可以暂停计算并且可以随后返回表达式的值的函数。

一个简单的例子使用 generator:

function* sillyGenerator() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
}

var generator = sillyGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: 4, done: false }

next 可以回去到下一个 yield 返回的值,当然上面的代码是非常不自然的,我们可以利用 Generator 来用同步的方式来写异步操作:

// Hiding asynchronousity with Generators

function request(url) {
    getJSON(url, function(response) {
        generator.next(response);
    });
}

这里的 generator 函数将会返回需要的数据:

function* getData() {
    var entry1 = yield request('http://some_api/item1');
    var data1  = JSON.parse(entry1);
    var entry2 = yield request('http://some_api/item2');
    var data2  = JSON.parse(entry2);
}

通过 yield,我们可以保证 data1data2 中我们需要解析并储存的数据。

虽然我们可以利用 Generator 来用同步的方式来写异步操作,但是确认错误的传播变得不再清晰,我们可以在 Generator 中加上 Promise:

function request(url) {
    return new Promise((resolve, reject) => {
        getJSON(url, resolve);
    });
}

然后我们写一个函数逐步调用 next 并且利用 request 方法产生一个 Promise:

function iterateGenerator(gen) {
    var generator = gen();
    (function iterate(val) {
        var ret = generator.next();
        if(!ret.done) {
            ret.value.then(iterate);
        }
    })();
}

在 Generator 中加上 Promise 之后我们可以更清晰的使用 Promise 中的 .catchreject来捕捉错误,让我们使用新的 Generator,和之前的还是蛮相似的:

iterateGenerator(function* getData() {
    var entry1 = yield request('http://some_api/item1');
    var data1  = JSON.parse(entry1);
    var entry2 = yield request('http://some_api/item2');
    var data2  = JSON.parse(entry2);
});

14. Async Await

这是 ES7 中的新特性,但是目前 babel 转码已经支持。

async await 让我们可以使用更精简明了的代码处理实现 Promise 和 Generator 所实现的异步处理:

var request = require('request');

function getJSON(url) {
  return new Promise(function(resolve, reject) {
    request(url, function(error, response, body) {
      resolve(body);
    });
  });
}

async function main() {
  var data = await getJSON();
  console.log(data); // NOT undefined!
}

main();

它内部所实现方式的和 Generator + Promise 的组合其实是一样的。

15. Getter/Setter 函数

ES6 已经开始实现了 gettersetter 函数,比如下面这个例子:

class Employee {
    constructor(name) {
        this._name = name;
    }

    get name() {
      if(this._name) {
        return 'Mr. ' + this._name.toUpperCase();  
      } else {
        return undefined;
      }  
    }

    set name(newName) {
      if (newName == this._name) {
        console.log('I already have this name.');
      } else if (newName) {
        this._name = newName;
      } else {
        return false;
      }
    }
}

var emp = new Employee("James Bond");

// uses the get method in the background
if (emp.name) {
  console.log(emp.name);  // Mr. JAMES BOND
}

// uses the setter in the background
emp.name = "Bond 007";
console.log(emp.name);  // Mr. BOND 007

最新版本的浏览器也在对象中实现了 gettersetter函数,我们可以使用它们来实现计算属性,在设置和获取一个属性之前加上监听器和处理。

var person = {
  firstName: 'James',
  lastName: 'Bond',
  get fullName() {
      console.log('Getting FullName');
      return this.firstName + ' ' + this.lastName;
  },
  set fullName (name) {
      console.log('Setting FullName');
      var words = name.toString().split(' ');
      this.firstName = words[0] || '';
      this.lastName = words[1] || '';
  }
}


person.fullName; // James Bond
person.fullName = 'Bond 007';
person.fullName; // Bond 007

比如 Vue2 中就是通过gettersetter函数实现的数据监听。