Classes

ECMAScript 6 引入了 JavaScript 类,给原有的JavaScript原型继承提供了语法糖. 类的语法并没有向JavaScript引进一个新的面向对象的继承模式. classes为创建对象,处理继承提供了简单,清晰的语法.

定义类

类其实是”特殊的函数”.就像你可以定义 表达式函数 function expressions声明式函数 function declaration 一样,类也有两种形式 表达式类声明式类

声明式类

定义类的方式之一就是声明式类. 要声明一个类,可以使用 class 关键字,加上类的名字(在这里叫”Ploygon”).

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

预声明

函数声明和类声明的一个重要区别在于,函数会预声明,而类不会.你必须先声明类,然后再使用它.像下面这样的代码会报错: ReferenceError.

var p = new Polygon(); // ReferenceError

class Polygon {}

表达式类

表达式类是定义类的另一种方式. 表达式类可以有名字也没有没有名字.给一个已经有名字的表达式类一个变量名,两个名字必须相等…(正常人都不会这样玩吧…)

// unnamed
var Polygon = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

// named
var Polygon = class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

类体和方法的定义

{}里的部分就被成为类体.这是你定义类内容的地方,比如类的方法和构造器.

严格模式

类体(无论是声明式类还是表达式类)里的代码,都是以严格模式执行的.

Constructor

constructor是一个特殊的方法,用于从所在的 class 创建和实例化对象.在一个类里,只能有一个名叫 “constructor” 的特殊方法.如果 constructor 方法在一个类里出现一次以上,会报错: SyntaxError

constructor可以使用 super 关键字来调用父类的constructor

原型方法

参考 方法定义(method definitions).

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }

  get area() {
    return this.calcArea();
  }

  calcArea() {
    return this.height * this.width;
  }
}

静态方法

static 关键字给类定义了静态方法.静态方法的调用不需要实例化类,并且实例化以后的实例不能调用静态方法.静态方法一般用于给应用提供工具函数.

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;

        return Math.sqrt(dx*dx + dy*dy);
    }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));

通过 extends 定义子类

extends关键字在声明式类和表达式类里都可以用,用于创建一个类的子类.

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

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}

通过 super 调用超类

super 关键字可以调用父类的方法

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

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

混合(Mix-ins)

抽象子类或混合,是类的模板.ECMAScript类只能拥有一个超类,所以,继承多个工具类是不可行的. 所有的工具函数都必须又超类提供.

想要在 ECMAScript 里实现混合,可以创造一个函数,它接受一个超类作为输入参数,然后输出一个继承该超类的子类:

var CalculatorMixin = Base => class extends Base {
  calc() { }
};

var RandomizerMixin = Base => class extends Base {
  randomize() { }
};

一个使用了这些混合的类看起来应该是这样的:

class Foo { }
class Bar extends CalculatorMixin(RandomizerMixin(Foo)) { }

这个可能比较难理解,因为我把以上代码转换成es5以后,发现代码非常多,非常长…所以还是先从概念上去理解…
首先,我把这个例子写的更深入一点:

"use strict";
var CalculatorMixin = Base => class extends Base {
  calc() { 
    return 'calcResult';
  }
};

var RandomizerMixin = Base => class extends Base {
  randomize() { 
    return 'randomizeResult'
  }
};

var Bunny = Base => class extends Base {
  bunny() { 
    return 'bunny'
  }
};

class Foo { }
class Bar extends CalculatorMixin(RandomizerMixin(Bunny(Foo))) { }

var bar = new Bar();
console.log(bar.calc());       //'calcResult'
console.log(bar.randomize());  //'dandomizeResult'
console.log(bar.bunny());      //'bunny'

通过这个例子,可以很好的明白,ECMAScript的”混合”是在做什么: 它可以让一个类去包含多个”混合”里的工具函数,然后实例化这个类的时候,实例也会继承来自多个”混合”的方法.

原文地址


end

destructuring assignment

Javascript的解构分配语法通过映射数组或字面量对象的语法来提取从数组或对象里提取数据.

语法

[a, b] = [1, 2]
[a, b, ...rest] = [1, 2, 3, 4, 5]
{a, b} = {a:1, b:2}
{a, b, ...rest} = {a:1, b:2, c:3, d:4}  //ES7

{a, b} = {a:1, b:2} 这个语法是不能独立使用的,因为左边的 {a,b} 会被认为是一个代码块,而不是一个字面量对象.
但是,({a, b} = {a:1, b:2}) 格式是可以的,它和 var {a, b} = {a:1, b"2} 一致.

描述

对象和数组字面量表达式为创建临时数据包提供了一种简单的方法. 一旦成功创建了数据包,你可以随心所欲的使用它.你甚至可以把它放在函数的返回值里.

使用解构分配有一个特别实用的功能: 你只需要通过一句代码就可以读取整个数据解构,可以用它做很多事情.下面会给出例子.

这个特点某些其他语言也有,比如 Perl 和 Python.

数据解构

简单的例子

var foo = ["one", "two", "three"];

// without destructuring
var one   = foo[0];
var two   = foo[1];
var three = foo[2];

// with destructuring
var [one, two, three] = foo;

免声明分配

在使用解构分配的时候,不一定要声明变量.

var a, b;

[a, b] = [1, 2];

变量交换

执行这段代码以后,b等于1,a等于3. 在没有解构分配功能之前,要实现这个操作需要一个临时变量

var a = 1;
var b = 3;

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

多变量返回值

多亏于解构分配,函数可以返回多个变量了.虽然它一直都可以返回一个数组,但是解构分配赋予了它更多使用灵活性.

function f() {
  return [1, 2];
}

可以看到,函数返回了一个像数组一样的东西,把所有的值都放在’[]’里.你可以使用这种方式返回任何数量的数据.在这个栗子里, f() 返回 [1,2] 作为输出.

var a, b;
[a, b] = f();
console.log("A is " + a + " B is " + b);

[a,b] = f() 这句代码会把函数返回的结果依次分配给’[]’里的变量: a被设置为1,b被设置为2.你也可以像以前一样把返回值当做数组用.

var a = f();
console.log("A is " + a);

在这个栗子里,a 是一个包含了1和2的数组.

忽略某些返回值

你还可以忽略某些你不需要的值:

function f() {
  return [1, 2, 3];
}

var [a, , b] = f();
console.log("A is " + a + " B is " + b);

执行这段代码以后, a等于1,b等于3. 2这个值就被忽略了.你可以使用这种方式来忽略一个或多个(甚至全部)返回值.举个栗子:

[,,] = f();

从正则表达式的匹配结果来拉取数据

当正则表达式通过 exec() 方法去匹配的时候,它会返回一个数组,数组的第一个值是整个被进行匹配的字符串,后面的值分别是匹配各个’()’里的内容得到的结果.解构分配能让你轻易地获取到数组里的各个部分内容,如果你不需要整个字符串,可以忽略它.

var url = "https://developer.mozilla.org/en-US/Web/JavaScript";

var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
var [, protocol, fullhost, fullpath] = parsedURL;

console.log(protocol); // logs "https"

对象解构

简单的例子

var o = {p: 42, q: true};
var {p, q} = o;

console.log(p); // 42
console.log(q); // true 

// 分配新的变量名
var {p: foo, q: bar} = o;

console.log(foo); // 42
console.log(bar); // true 

免声明分配

同样,在使用解构分配对象的时候,也不一定要声明.

var a, b;

({a, b} = {a:1, b:2});

在使用免声明对象字面量解构分配时,包围语句的’( .. )’必须要有.

定义函数参数默认值

ES5版本

function drawES5Chart(options) {
  options = options === undefined ? {} : options;
  var size = options.size === undefined ? 'big' : options.size;
  var cords = options.cords === undefined ? { x: 0, y: 0 } : options.cords;
  var radius = options.radius === undefined ? 25 : options.radius;
  console.log(size, cords, radius);
  // now finally do some chart drawing
}

drawES5Chart({
  cords: { x: 18, y: 30 },
  radius: 30
});

ES6版本

function drawES6Chart({size: size = 'big', cords: cords = { x: 0, y: 0 }, radius: radius = 25} = {}) 
{
  console.log(size, cords, radius);
  // do some chart drawing
}

drawES6Chart({
  cords: { x: 18, y: 30 },
  radius: 30
});

在火狐里,解构分配的默认值功能还没有被支持: var { x = 3 } = {} 以及 var [foo = "bar"] = [].查看关于函数参数解构分配默认值问题的bug: bug 932080;

模块化加载(非ES6方法)

解构分配可以加载指定的非ES6子组件,比如这里,使用插件SDK

const { Loader, main } = require('toolkit/loader');

嵌套对象和数组的解构分配

var metadata = {
    title: "Scratchpad",
    translations: [
       {
        locale: "de",
        localization_tags: [ ],
        last_edit: "2014-04-14T08:43:37",
        url: "/de/docs/Tools/Scratchpad",
        title: "JavaScript-Umgebung"
       }
    ],
    url: "/en-US/docs/Tools/Scratchpad"
};

var { title: englishTitle, translations: [{ title: localeTitle }] } = metadata;

console.log(englishTitle); // "Scratchpad"
console.log(localeTitle);  // "JavaScript-Umgebung"

for循环里的解构分配

var people = [
    {
     name: "Mike Smith",
     family: {
       mother: "Jane Smith",
       father: "Harry Smith",
       sister: "Samantha Smith"
     },
     age: 35
    },
    {
     name: "Tom Jones",
     family: {
       mother: "Norah Jones",
       father: "Richard Jones",
       brother: "Howard Jones"
     },
     age: 25
    }
];

for (var {name: n, family: { father: f } } of people) {
    console.log("Name: " + n + ", Father: " + f);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

从函数参数对象里获取对应属性值

function userId({id}) {
  return id;
}

function whois({displayName: displayName, fullName: {firstName: name}}){
  console.log(displayName + " is " + name);
}

var user = { 
  id: 42, 
  displayName: "jdoe",
  fullName: { 
      firstName: "John",
      lastName: "Doe"
  }
};

console.log("userId: " + userId(user)); // "userId: 42"
whois(user); // "jdoe is John"

这样可以获取对象里的 id,displayName,firstName 属性,然后打印出来.

计算后的属性名和解构分配

和字面量对象一样,计算后的属性名也可以用于结构分配(就是下标型的属性名)

let key = "z";
let { [key]: foo } = { z: "bar" };

console.log(foo); // "bar"

原文地址


end

Arrow Functions (箭头函数) 详解

一个箭头函数表达式(也被成为大箭头函数)是普通函数表达式的简写,它通过词法绑定来指定 this 指针的值(不会绑定自己的this,arguments,super,或是 new.target). 箭头函数总是匿名的.

语法

基础语法

(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
// 相当于: => { return expression; }
// 当只有一个参数的时候,’()’是可选的:
(singleParam) => { statements }
singleParam => { statements }
// 没有参数的函数必须要使用’()’
() => { statements }

高级语法

// 如果需要返回一个字面量对象,需要用’()’包括它.
params => ({foo: bar})
// 支持 Rest parameters 以及 default parameters
(param1, param2, …rest) => { statements }
(param1 = defaultValue1, param2, …, paramN = defaultValueN) => { statements }
// 参数列表里还支持使用 解构赋值 destructuring
var f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f(); // 6

更多语法例子查看: 这里

描述

也可以看这篇: “ES6 In Depth: Arrow functions” on hacks.mozilla.org

箭头函数引入了两个关键的影响因素: 简短函数和词法指针(this).

简短函数

在一些函数模型下,简短函数很受欢迎.比较以下内容:

var a = [
  "Hydrogen",
  "Helium",
  "Lithium",
  "Beryl­lium"
];

var a2 = a.map(function(s){ return s.length });

var a3 = a.map( s => s.length );

词法绑定指针(this)

在箭头函数出现之前,每个函数的实例都有自己的指针(如果是一个构造函数,它就是实例化后的对象,如果是在严格模式下,直接调用函数,它就是undefined,如果是作为对象的方法被调用,它就是对象的上下文…等等…),这在面向对象编程里,是一个很麻烦的东西.

function Person() {
  // Person() 构造函数里的 `this` 指向它自己的实例对象.
  this.age = 0;

  setInterval(function growUp() {
    // 在非严格模式下, growUp()函数定义 this 指针指向全局对象,而不是Person()构造函数里的this.
    this.age++;
  }, 1000);
}

var p = new Person();

在 ECMAScript 3/5 里, 通过把this赋值给一个新的封闭的变量,可以解决这个问题.

function Person() {
  var self = this; // 有些人习惯用 that,而不是 self. 
                   // 习惯了用哪个就一直用下去.
  self.age = 0;

  setInterval(function growUp() {
    // 回调里的self的值就是你期望的那个.
    self.age++;
  }, 1000);
}

另外,也可以创建一个 绑定了this的函数 bound function, 这样,正确的 this 值就会被传递到 growUp() 函数里.

箭头函数会捕捉到当前闭包环境下的 this 值,所以下面的代码会按照期望的那样运行:

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| 正确的指向了person对象
  }, 1000);
}

var p = new Person();

和严格模式的关系

假设 this 是词法绑定的,严格模式下关于 this 的相关规则会被忽视.

var f = () => {'use strict'; return this};
f() === window; // 或者是全局对象

其余严格模式规则都正常执行.

通过 call 或者 apply 调用

由于 this 已经被词法绑定了,通过 call()apply() 调用只能起到传递参数的作用,不能改变 this 指针的指向:

var adder = {
  base : 1,

  add : function(a) {
    var f = v => v + this.base;
    return f(a);
  },

  addThruCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base : 2
    };

    return f.call(b, a);
  }
};

console.log(adder.add(1));         // 2
console.log(adder.addThruCall(1)); // 还是2

词法绑定 arguments

箭头函数不会把参数对象 arguments 暴露给代码,所以 arguments.length, arguments[0], arguments[1] 等得到的值和 this 一样,是词法绑定的,也就是说,它指向的是调用函数的闭包作用域下的 arguments 变量.

var arguments = 42;
var arr = () => arguments;

arr(); // 42

function foo() {
  var f = (i) => arguments[0]+i; // foo函数里隐式的绑定了arguments
  return f(2);
}

foo(1); // 3

箭头函数没有自己的 arguments 对象,但是在大多数情况下,使用 rest parameters 是一个很好的选择:

function foo() { 
  var f = (...args) => args[0]; 
  return f(2); 
}

foo(1); // 2

使用 yield 关键词

yield关键词不能被用在箭头函数的函数体里.(除了用在函数里嵌套的函数里)所以,箭头函数不能被作为一个生成器.

函数体

箭头函数既可以使用 “简约格式” 也可以使用普通的 “代码块” 作为函数体

使用代码块格式不会自动返回一个值.你需要明确地使用 return 语句.

var func = x => x * x;                  // 简洁语法, 隐式调用 "return"
var func = (x, y) => { return x + y; }; // 通过代码块, 需要显式地使用 "return" 

返回字面量对象

记住,使用简洁语法来返回字面量对象 params => {object:literal} 不会像你预期的那样执行:

var func = () => {  foo: 1  };               // 调用 func() 返回 undefined!
var func = () => {  foo: function() {}  };   // SyntaxError: function statement requires a name

这是因为花括号里内容会被当做语句顺序解析.(比如, foo 会被看做一个标签,而不是字面量对象的键)

记得,把字面量对象用’()’包起来:

var func = () => ({ foo: 1 });

一些栗子

// empty 箭头函数返回 undefined
let empty = () => {};

(() => "foobar")() // 返回 "foobar" 

var simple = a => a > 15 ? 15 : a; 
simple(16); // 15
simple(10); // 10

let max = (a, b) => a > b ? a : b;

// 简单的实现数组的过滤,迭代, ...

var arr = [5, 6, 13, 0, 1, 18, 23];
var sum = arr.reduce((a, b) => a + b);  // 66
var even = arr.filter(v => v % 2 == 0); // [6, 0, 18]
var double = arr.map(v => v * 2);       // [10, 12, 26, 0, 2, 36, 46]

// 更简约的promise链
promise.then(a => {
  // ...
}).then(b => {
   // ...
});

原文地址


end

ECMAScript6 let 详解

let 语句声明一个块级作用域变量,它的初始化值为可选的.

语法

let var1 [= value1] [, var2 [= value2]] [, …, varN [= valueN]];

参数

var1, var2, …, varN
变量名.可以是任何合法的标识符.

value1, value2, …, valueN
变量的初始值.可以是任何合法的表达式.

描述

let 声明变量的作用域为语句或表达式所在的块.这和使用 var 语句不同, var 声明的变量是全局变量,或者整个函数范围里的变量,而不是所在的代码块作用域.

块级作用域和 let

在一个代码块里使用 let 语句定义变量

if (x > y) {
  let gamma = 12.7 + y;
  i = gamma * x;
}

通常, let 语句用在函数里嵌套函数的时候能使得代码更清晰.

var list = document.getElementById("list");

for (var i = 1; i <= 5; i++) {
  var item = document.createElement("LI");
  item.appendChild(document.createTextNode("Item " + i));

  let j = i;
  item.onclick = function (ev) {
    console.log("Item " + j + " is clicked.");
  };
  list.appendChild(item);
}

上面的这个例子能够按照所希望的那样运行,因为五个内嵌的匿名函数的实例里分别使用五个不同的变量 j 的实例.注意,如果你把 let 换成 var 或者移除变量 j 或者在内嵌的函数里使用 i ,是不行的.

作用域规则

通过 let 声明的变量它的作用域是声明时所在的块级作用域,以及块级作用域的子块级作用域.就这一点来说, let 的工作方式和 var 很相似. 他们两者间最主要的区别是: var 声明的变量所在的作用域是整个闭包函数.

function varTest() {
  var x = 31;
  if (true) {
    var x = 71;  // x是同一个变量!
    console.log(x);  // 71
  }
  console.log(x);  // 71
}

function letTest() {
  let x = 31;
  if (true) {
    let x = 71;  // x是两个不同的变量
    console.log(x);  // 71
  }
  console.log(x);  // 31
}

不同于使用 var , 如果在程序或者函数的最外层使用 let 语句,它不会给全局对象创建一个新的属性.举个栗子:

var x = 'global';
let y = 'global';
console.log(this.x);   //全局对象下有x属性
console.log(this.y);   //全局对象下没有y属性

这段代码运行的结果: this.x 的输出为 "global" ,但 this.y 的输出为 undefined.

暂死区(temporal dead zone)和let相关报错

在同一个函数或者代码块里使用 let 声明两个同样的变量会导致 TypeError.

if (x) {
  let foo;
  let foo; // TypeError thrown.
}

ECMAScript 2015 会把通过 let 语句声明的变量提升到代码块的开头(类似于var声明变量的预解析). 然而,在变量声明之前访问这个变量会导致ReferenceError.
因为存在于代码块开头的变量还处在”暂死区”里,直到程序走到声明变量那里.

补充说明一下:众所周知,var 声明的变量会被预声明,所以在 var 之前访问变量,不会报错,而是得到 undefined. 同样, let 声明的变量也会被提升到区块的顶部,但是却不能访问它,因为变量虽然声明了,但还处在 “暂死区” 里,直到程序走到 let 声明变量的语句,变量才从暂死区里被释放出来. 大致就是这样,点击了解更多关于暂死区的概念.

function do_something() {
  console.log(foo); // ReferenceError
  let foo = 2;
}

如果你在 switch 语句中使用 let ,可能会遇到错误,因为在 switch 语法里,它只有一个底层代码块

switch (x) {
  case 0:
    let foo;
    break;

  case 1:
    let foo; // 由于重复声明导致 TypeError .
    break;
}

for 循环里使用 let 时的作用域

你可以在 for 循环里使用 let 语句来创建一个对应当前作用域的变量.这和在 for 循环头部使用 var 语句是不同的, var 声明的变量在不仅在循环里可用,在整个所在的函数里都可用.

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

注意这个例子是错的,它会抛出 ReferenceError 错误,在介绍 暂死区 的文章里有类似的例子…大致解释一下:变量 i 一开始就被预提升到整个块作用域了,但是还处在暂死区,然后当执行 let i=i 的时候,要给变量 i 进行词法绑定,但是绑定的值又是 i, 而 i 又处在暂死区,无法访问,所以就报错了.但是需要注意的是,由于 i 变量已经被预提升了,它不是一个不存在的变量,而是一个处在暂死区的变量,所以,它不会沿着作用域链向上寻找到外层 var 声明的 i,而是直接报错了.
然后这个例子正确的写法应该是:

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

作用域规则

for (let expr1; expr2; expr3) statement

在这个例子里, expr2,expr3 以及 statement 语句,都会被隐式的包含到一个代码块里. let 声明的 expr1 变量就处在这个隐藏的代码块里. 这在上一个循环的例子里已经阐述过了.

栗子

let vs var

在代码块里使用 let 语句,变量的作用域就是所在的代码块. 而 var 声明的变量,所在的作用域是声明时所在的整个函数.

var a = 5;
var b = 10;

if (a === 5) {
  let a = 4; // 作用域是在 if 代码块里
  var b = 1; // 作用域是在整个函数里

  console.log(a);  // 4
  console.log(b);  // 1
} 

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

循环里的 let

建议在循环里使用 let 来创建一个作用于循环代码块下的变量,而不是使用 var 来创建一个全局的变量用于循环.

for (let i = 0; i<10; i++) {
  console.log(i); // 0, 1, 2, 3, 4 ... 9
}

console.log(i); // i is not defined

非标准的 let 扩展

非标准的 let 扩展基本没有得到支持,而且很多已经被废弃,就不翻译了…..((☄⊙ω⊙)☄…好吧,因为快要下班了…)

原文地址


end

Object.defineProperty()方法

Object.defineProperty()方法详解

查看原文

Object.defineProperty() 方法可以直接给对象定义一个新的属性, 或者修改对象上已存在的某个属性, 最后返回这个对象.

语法

Object.defineProperty(obj, prop, descriptor)

Parameters

obj
需要被定义属性的对象

prop
需要被定义(或修改)的属性名

descriptor
对需要被定义(或修改)的属性的描述

介绍

这个方法允许精确的添加或修改对象的属性.通过普通的分配方式给对象添加的属性,会在对象属性被枚举时展示(比如for in循环或者使用Object.keys),它的值也可以被修改,被delete.这个方法允许你通过更详细的配置来修改这些默认的行为. 默认情况下,通过 Object.defineProperty() 定义的属性值是不可变的.

对象的属性的描述符(即descriptor参数)有两种方式定义: 数据描述符(data descriptor)和存取器描述符(accessor descriptor).
数据描述符表示:这个属性有一个值,这个值可以是可写的,也可以是不可写的.
存取器描述符表示:这个属性有一对函数,一个是获取函数,一个是设置函数(getter&setter);
描述符必须是这两者之一,不能都有.

数据描述符和存取器描述符都是对象. 他们都需要下列属性:

configurable
true: 当且仅当描述符可以被修改并且属性可以从相应的对象中被删除的时候
默认是false.

enumerable
true: 当且仅当这个属性需要在对象属性被枚举的时候被展示出来.
默认是false.

数据描述符还拥有下列可选的属性:

value
属性对应的值.可以是任何有效的Javascript值(数值,对象,函数,等等).
默认是undefined.

writable
true: 当且仅当属性的值可以通过赋值操作而被改变时.
默认是false

存取器描述符还拥有下列可选的属性:

get
一个函数,它是属性的获取函数.如果没有定义获取函数,它就是undefined.函数的返回值就会成为这个属性的值.
默认是undefined.

set
一个函数,它是属性的设置函数,如果没有定义设置函数,它就是undefined.当属性值被赋值(通过普通方式)的时候,这个新值会被作为唯一的参数传入.
默认是undefined.

记住,上面这些选项不一定要是对象自身的属性,所以还需要考虑是否被继承的问题.为了确保在不写这些选项时,它取到的是默认值,你需要预先冻结Object.prototype. 所以,明确的指定每个选项,或者把 __proto__ 属性指定为 null.

// 冻结 __proto__
var obj = {};
Object.defineProperty(obj, 'key', {
  __proto__: null, // no inherited properties
  value: 'static'  // not enumerable
                   // not configurable
                   // not writable
                   // as defaults
});

// 明确指定每个选项值
Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'static'
});

// 重用同一个对象
function withValue(value) {
  var d = withValue.d || (
    withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value: null
    }
  );
  d.value = value;
  return d;
}
// ... 然后 ...
Object.defineProperty(obj, 'key', withValue('static'));

// 如果freeze方法是可用的,阻止对对象 prototype 属性进行新增或删除.(value, get, set, enumerable, writable, configurable)   
(Object.freeze || Object)(Object.prototype);

Examples

如果你想知道如何使用二进制标识风格句法来使用 Object.defineProperty 方法,查看额外例子 (这个好恶心…━┳━ ━┳━)

新建一个属性

当对象里没有既存的指定属性时,Object.defineProperty()方法会按照描述创建一个新的属性.描述符里的所有选项都可以省略,被省略的选项会取默认值.所有的布尔值都默认为false. value,get,set选项默认是undefined.如果一个属性,它的描述符里没有 get/set/value/writable 这些选项,它被称为 ‘通用型(generic)’, 并且被归类到数据描述符.

var o = {}; // 创建一个新的对象

// 一个通过数据描述符,使用defineProperty来添加属性的例子
Object.defineProperty(o, 'a', {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true
});

// 'a' 属性存在于 o 对象中,它的值是37 

// 一个通过存取器描述符,使用defineProperty来添加属性的例子 
var bValue = 38;
Object.defineProperty(o, 'b', {
  get: function() { return bValue; },
  set: function(newValue) { bValue = newValue; },
  enumerable: true,
  configurable: true
});
o.b; // 38
// 'b' 属性存在于 o 对象中,它的值是38
// 现在, o.b的值永远等于bValue,除非重新定义o.b

// 你不能尝试结合两者:
Object.defineProperty(o, 'conflict', {
  value: 0x9f91102,
  get: function() { return 0xdeadbeef; }
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors

补充说一下这里的set选项,get选项很简单,就是通过这个函数来获取对象对应的属性值,而set函数,是当你使用 . 操作符来给属性分配值的时候被调用的. 需要注意的是,当使用 . 操作符来给属性分配值的时候,先调用 setter ,然后还会调用 getter.考虑以下代码:

Object.defineProperty(o, 'b', {
  get: function() { return bValue; },
  set: function(newValue) { bValue = newValue+1; },
  enumerable: true,
  configurable: true
});
o.b; // 38
o.b = 39;
console.log(o.b) //40

当使用 o.b = 39 来给对象分配属性值时, 首先调用 setter ,然后调用 getter, 而o.b最终的值,依然是 getter 得到的结果.

修改属性

当属性已经存在, Object.defineProperty() 会尝试根据描述符里的值和对象目前的配置来修改这个属性. 如果原来的描述符里,configurable 属性被设置为 false (代表这个属性不能被配置),那么,除了writable之外的所有属性都不能被修改.在这样的情况下,也不能切换描述符的类型,不能把数据描述符改成存取器描述符,反之也不能.

如果一个属性是不可配置的,它的 writable 属性只能被修改成 false.

尝试修改一个不可配置的属性(除了 writable),除非修改后的值和原来一样,否则会抛出一个 TypeError .

Writable 属性

writable 属性被设置为 false 的时候, 表示这个属性是不可写的. 它不能被重定义值.

var o = {}; // 创建一个新的对象

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});

console.log(o.a); // 打印出 37
o.a = 25; // 不会报错(如果是在严格模式下还是会报错,即使你定义的值和原来一样,也会报错)
console.log(o.a); // 打印出 37. 之前的定义没有生效

就如这个栗子所见, 尝试写入一个不可写的属性不会生效,但也不会报错

虽然不能重写,但是是否可以删除取决于configurable,而不是writable

Enumerable 属性

enumerable 属性定义了对象属性在使用for…in循环或者Object.keys()枚举时是否被展示

var o = {};
Object.defineProperty(o, 'a', { value: 1, enumerable: true });
Object.defineProperty(o, 'b', { value: 2, enumerable: false });
Object.defineProperty(o, 'c', { value: 3 }); // enumerable 默认为false
o.d = 4; // 使用这种方式设置的时候,enumerable 默认为true

for (var i in o) {
  console.log(i);
}
// 打印出 'a' 和 'd' (in undefined order)

Object.keys(o); // ['a', 'd']

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false

Configurable 属性

configurable 同时控制了该属性是否能从对象中被删除,以及(描述符里的)属性(除了writable)是否可以被修改.

var o = {};
Object.defineProperty(o, 'a', {
  get: function() { return 1; },
  configurable: false
});

Object.defineProperty(o, 'a', { configurable: true }); // throws a TypeError
Object.defineProperty(o, 'a', { enumerable: true }); // throws a TypeError
Object.defineProperty(o, 'a', { set: function() {} }); // throws a TypeError (set属性值之前是undefined)
Object.defineProperty(o, 'a', { get: function() { return 1; } }); // throws a TypeError (即使新的函数做的事情和原来的一模一样)
Object.defineProperty(o, 'a', { value: 12 }); // throws a TypeError

console.log(o.a); // logs 1
delete o.a; // 什么也没发生
console.log(o.a); // logs 1

如果 o.aconfigurable 属性被设置为 true , 以上报错都不会有,最后这个属性会被删除.

添加对象属性时的默认值

有一个重要的事需要被考虑到:在你为对象添加属性的时候,属性的默认属性是怎么样的.通常来说,简单地使用 . 操作符和使用 Object.defineProperty() 来分配属性值是有区别的,如下栗子所示:

var o = {};

o.a = 1;
// 也可以写成:
Object.defineProperty(o, 'a', {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true
});


// 另外,
Object.defineProperty(o, 'a', { value: 1 });
// 也可以写成:
Object.defineProperty(o, 'a', {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

自定义 Setters 和 Getters

下面的例子展示了如何实现一个自动存档的对象.每当温度属性被设置, 存档数组都会有相应的记录.

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

版本说明

版本 状态 备注
‘Object.defineProperty’方法被定义在了ECMAScript 5.1 (ECMA-262) ST 标准 初次定义. 在JavaScript 1.8.5 里被实现
‘Object.defineProperty’方法被定义在了ECMAScript 2015 (6th Edition, ECMA-262) ST 标准
‘Object.defineProperty’方法被定义在了ECMAScript 2016 Draft (7th Edition, ECMA-262) D 草案

浏览器兼容性

桌面端

特性 Firefox (Gecko) Chrome Internet Explorer Opera Safari
基本支持 4.0(2) 5 9[1] 11.60 5.1[2]

[1] 在ie8下只支持DOM对象, 而且有一些非标准行为.
[2] Safari5 也支持, 但不支持DOM对象.

移动端

特性 Firefox Mobile (Gecko) Android IE Mobile Opera Mobile Safari Mobile
基本支持 4.0(2) (支持) 9 11.5 (支持)

兼容性问题补充说明

重新定义数组对象的 length 属性

重新定义数组的 length 属性是可行的,但也受制于普通的重定义时的限制.(length属性一开始是不能配置,不可枚举,但是可写的.所以,改变一个没有被修改过(描述符)的数组的 length 属性是可行的.也可以把它改成不可写的,但是不允许修改它的可枚举性和可配置性.如果它已经被改成不可写的,也不能修改它的值,也不能修改它的可写性.)然而,并不是所有的浏览器都允许重定义它的.

如果在 Firefox 4 到 22 里尝试此操作会抛出一个 TypeError , 无论它是否允许重定义数组的 length 属性.

某些版本的Chrome浏览器在使用 Object.defineProperty() 来修改数组 length 属性时,如果修改后的值不同于数组当前的 length 属性,它会忽略这次修改.在某些情况下,修改它的可写性其实也没有生效(也不会报错).另外,与此相关的是,一些修改数组的方法,比如 Array.prototype.push 却无视 length 属性的不可写性.(就是说,使用push方法,数组的length属性值依然会被改变,即使它是不可写的.)

某些版本的Safari浏览器在使用 Object.defineProperty() 来修改数组 length 属性时,如果修改后的值不同于数组当前的 length 属性,它会忽略这次修改,还会尝试去改变它的可写性,但最终,属性的可写性并没有被改变,这个过程也不会报错.

只有ie 9 及以后的版本, Firefox 23 及以后的版本, 开始完全地,正确地支持重定义数组的 length 属性.到目前为止,不要指望使用 Object.defineProperty() 重定义数组的 length 属性能够以一种通用的方式正常工作.而且,就算它靠得住,也没有足够的理由去这样做.

针对ie8的特别说明

ie 8 下的 Object.defineProperty() 方法只能被用在DOM对象上.有一些事情需要注意:

  • 如果尝试在原生的对象上使用 Object.defineProperty() 方法,会报错.
  • 定义属性时,描述符的选项必须被设置为指定的值. 如果是数据描述符,configurable,enumerable,writable 都必须被设置为true,如果是存取器描述符, configurable 必须是 true, enumerable 必须是 false, 如果设置的值不是这些,会报错.
  • 重定义属性的话,需要把原来的属性给删掉.如果原来的属性没有删掉,重定义不会生效,它还是保持原来的属性.

看看相关内容


end