在ES2015规范敲定而且Node.js增添了大量的函数式子集的背景下,咱们终于能够拍着胸脯说:将来就在眼前。javascript
… 我早就想这样说了html
但这是真的。V8引擎将很快实现规范,并且Node已经添加了大量可用于生产环境的ES2015特性。下面要列出的是一些我认为颇有必要的特性,并且这些特性是不使用须要像Babel或者Traceur这样的翻译器就能够直接使用的。java
这篇文章将会讲到三个至关流行的ES2015特性,而且已经在Node中支持了了:git
用let和const声明块级做用域;github
箭头函数;flask
简写属性和方法。设计模式
让咱们立刻开始。数组
做用域是你程序中变量可见的区域。换句话说就是一系列的规则,它们决定了你声明的变量在哪里是可使用的。promise
你们应该都听过 ,在JavaScript中只有在函数内部才会创造新的做用域。然而你建立的98%的做用域事实上都是函数做用域,其实在JavaScript中有三种建立新做用域的方法。你能够这样:缓存
建立一个函数。你应该已经知道这种方式。
建立一个catch块。 我绝对没哟开玩笑.
建立一个代码块。若是你用的是ES2015,在一段代码块中用let或者const声明的变量会限制它们只在这个块中可见。这叫作块级做用域。
一个代码块就是你用花括号包起来的部分。 { 像这样 }。在if/else声明和try/catch/finally块中常常出现。若是你想利用块做用域的优点,你能够用花括号包裹任意的代码来建立一个代码块
考虑下面的代码片断。
// 在 Node 中你须要使用 strict 模式尝试这个 "use strict"; var foo = "foo"; function baz() { if (foo) { var bar = "bar"; let foobar = foo + bar; } // foo 和 bar 这里均可见 console.log("This situation is " + foo + bar + ". I'm going home."); try { console.log("This log statement is " + foobar + "! It threw a ReferenceError at me!"); } catch (err) { console.log("You got a " + err + "; no dice."); } try { console.log("Just to prove to you that " + err + " doesn't exit outside of the above `catch` block."); } catch (err) { console.log("Told you so."); } } baz(); try { console.log(invisible); } catch (err) { console.log("invisible hasn't been declared, yet, so we get a " + err); } let invisible = "You can't see me, yet"; // let 声明的变量在声明前是不可访问的
还有些要强调的
注意foobar在if块以外是不可见的,由于咱们没有用let声明;
咱们能够在任何地方使用foo ,由于咱们用var定义它为全局做用域可见;
咱们能够在baz内部任何地方使用bar, 由于var-声明的变量是在定义的整个做用域内均可见。
用let or const声明的变量不能在定义前调用。换句话说,它不会像var变量同样被编译器提高到做用域的开始处。
const 与 let 相似,但有两点不一样。
必须给声明为const的变量在声明时赋值。不能够先声明后赋值。
不能改变const变量的值,只有在建立它时能够给它赋值。若是你试图改变它的值,会获得一个TyepError。
咱们已经用var将就了二十多年了,你可能在想咱们真的须要新的类型声明关键字吗?(这里做者应该是想表达这个意思)
问的好,简单的回答就是–不, 并不真正须要。但在能够用let和const的地方使用它们颇有好处的。
let和const声明变量时都不会被提高到做用域开始的地方,这样可使代码可读性更强,制造尽量少的迷惑。
它会尽量的约束变量的做用域,有助于减小使人迷惑的命名冲突。
这样可让程序只有在必须从新分配变量的状况下从新分配变量。 const 能够增强常量的引用。
另外一个例子就是 let 在 for 循环中的使用:
"use strict"; var languages = ['Danish', 'Norwegian', 'Swedish']; //会污染全局变量! for (var i = 0; i < languages.length; i += 1) { console.log(`${languages[i]} is a Scandinavian language.`); } console.log(i); // 4 for (let j = 0; j < languages.length; j += 1) { console.log(`${languages[j]} is a Scandinavian language.`); } try { console.log(j); // Reference error } catch (err) { console.log(`You got a ${err}; no dice.`); }
在for循环中使用var声明的计数器并不会真正把计数器的值限制在本次循环中。 而let能够。
let在每次迭代时从新绑定循环变量有很大的优点,这样每一个循环中拷贝自身 , 而不是共享全局范围内的变量。
"use strict"; // 简洁明了 for (let i = 1; i < 6; i += 1) { setTimeout(function() { console.log("I've waited " + i + " seconds!"); }, 1000 * i); } // 功能彻底混乱 for (var j = 0; j < 6; j += 1) { setTimeout(function() { console.log("I've waited " + j + " seconds for this!"); }, 1000 * j); }
第一层循环会和你想象的同样工做。而下面的会每秒输出 “I’ve waited 6 seconds!”。
好吧,我选择狗带。
JavaScript的this关键字由于老是不按套路出牌而臭名昭著。
事实上,它的规则至关简单。无论怎么说,this在有些情形下会致使奇怪的用法
"use strict"; const polyglot = { name : "Michel Thomas", languages : ["Spanish", "French", "Italian", "German", "Polish"], introduce : function () { // this.name is "Michel Thomas" const self = this; this.languages.forEach(function(language) { // this.name is undefined, so we have to use our saved "self" variable console.log("My name is " + self.name + ", and I speak " + language + "."); }); } } polyglot.introduce();
在introduce里, this.name是undefined。在回调函数外面,也就是forEach中, 它指向了polyglot对象。在这种情形下咱们老是但愿在函数内部this和函数外部的this指向同一个对象。
问题是在JavaScript中函数会根据肯定性四原则在调用时定义本身的this变量。这就是著名的动态this 机制。
这些规则中没有一个是关于查找this所描述的“附近做用域”的;也就是说并无一个确切的方法可让JavaScript引擎可以基于包裹做用域来定义this的含义。
这就意味着当引擎查找this的值时,能够找到值,但却和回调函数以外的不是同一个值。有两种传统的方案能够解决这个问题。
在函数外面把this保存到一个变量中,一般取名self,并在内部函数中使用;
或者在内部函数中调用bind阻止对this的赋值。
以上两种办法都可生效,但会产生反作用。
另外一方面,若是内部函数没有设置它本身的this值,JavaScript会像查找其它变量那样查找this的值:经过遍历父做用域直到找到同名的变量。这样会让咱们使用附近做用域代码中的this值,这就是著名的词法this。
若是有样的特性,咱们的代码将会更加的清晰,不是吗?
在 ES2015 中,咱们有了这一特性。箭头函数不会绑定this值,容许咱们利用词法绑定this关键字。这样咱们就能够像这样重构上面的代码了:
"use strict"; let polyglot = { name : "Michel Thomas", languages : ["Spanish", "French", "Italian", "German", "Polish"], introduce : function () { this.languages.forEach((language) => { console.log("My name is " + this.name + ", and I speak " + language + "."); }); } }
… 这样就会按照咱们想的那样工做了。
箭头函数有一些新的语法。
"use strict"; let languages = ["Spanish", "French", "Italian", "German", "Polish"]; // 多行箭头函数必须使用花括号, // 必须明确包含返回值语句 let languages_lower = languages.map((language) => { return language.toLowerCase() }); // 单行箭头函数,花括号是可省的, // 函数默认返回最后一个表达式的值 // 你能够指明返回语句,这是可选的。 let languages_lower = languages.map((language) => language.toLowerCase()); // 若是你的箭头函数只有一个参数,能够省略括号 let languages_lower = languages.map(language => language.toLowerCase()); // 若是箭头函数有多个参数,必须用圆括号包裹 let languages_lower = languages.map((language, unused_param) => language.toLowerCase()); console.log(languages_lower); // ["spanish", "french", "italian", "german", "polish"] // 最后,若是你的函数没有参数,你必须在箭头前加上空的括号。 (() => alert("Hello!"))();
MDN关于箭头函数的文档解释的很好。
ES2015提供了在对象上定义属性和方法的一些新方式。
在 JavaScript 中, method 是对象的一个有函数值的属性:
"use strict"; const myObject = { const foo = function () { console.log('bar'); }, }
在ES2015中,咱们能够这样简写:
"use strict"; const myObject = { foo () { console.log('bar'); }, * range (from, to) { while (from < to) { if (from === to) return ++from; else yield from ++; } } }
注意你也可使用生成器去定义方法。只须要在函数名前面加一个星号(*)。
这些叫作 方法定义 。和传统的函数做为属性很像,但有一些不一样:
只能在方法定义处调用super;
不容许用new调用方法定义。
我会在随后的几篇文章中讲到super关键字。若是你等不及了, Exploring ES6中有关于它的干货。
ES6还引入了简写和推导属性 。
若是对象的键值和变量名是一致的,那么你能够仅用变量名来初始化你的对象,而不是定义冗余的键值对。
"use strict"; const foo = 'foo'; const bar = 'bar'; // 旧语法 const myObject = { foo : foo, bar : bar }; // 新语法 const myObject = { foo, bar }
两中语法都以foo和bar键值指向foo and bar变量。后面的方式语义上更加一致;这只是个语法糖。
当用揭示模块模式来定义一些简洁的公共 API 的定义,我经常利用简写属性的优点。
"use strict"; function Module () { function foo () { return 'foo'; } function bar () { return 'bar'; } // 这样写: const publicAPI = { foo, bar } /* 不要这样写: const publicAPI = { foo : foo, bar : bar } */ return publicAPI; };
这里咱们建立并返回了一个publicAPI对象,键值foo指向foo方法,键值bar指向bar方法。
这是不常见的例子,但ES6容许你用表达式作属性名。
"use strict"; const myObj = { // 设置属性名为 foo 函数的返回值 [foo ()] () { return 'foo'; } }; function foo () { return 'foo'; } console.log(myObj.foo() ); // 'foo'
根据Dr. Raushmayer在Exploring ES6中讲的,这种特性最主要的用途是设置属性名与Symbol值同样。
最后,我想提一下get和set方法,它们在ES5中就已经支持了。
"use strict"; // 例子采用的是 MDN's 上关于 getter 的内容 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get const speakingObj = { // 记录 “speak” 方法调用过多少次 words : [], speak (word) { this.words.push(word); console.log('speakingObj says ' + word + '!'); }, get called () { // 返回最新的单词 const words = this.words; if (!words.length) return 'speakingObj hasn\'t spoken, yet.'; else return words[words.length - 1]; } }; console.log(speakingObj.called); // 'speakingObj hasn't spoken, yet.' speakingObj.speak('blargh'); // 'speakingObj says blargh!' console.log(speakingObj.called); // 'blargh'
使用getters时要记得下面这些:
Getters不接受参数;
属性名不能够和getter函数重名;
能够用Object.defineProperty(OBJECT, "property name", { get : function () { . . . } }) 动态建立 getter
做为最后这点的例子,咱们能够这样定义上面的 getter 方法:
"use strict"; const speakingObj = { // 记录 “speak” 方法调用过多少次 words : [], speak (word) { this.words.push(word); console.log('speakingObj says ' + word + '!'); } }; // 这只是为了证实观点。我是绝对不会这样写的 function called () { // 返回新的单词 const words = this.words; if (!words.length) return 'speakingObj hasn\'t spoken, yet.'; else return words[words.length - 1]; }; Object.defineProperty(speakingObj, "called", get : getCalled ) 除了 getters,还有 setters。像日常同样,它们经过自定义的逻辑给对象设置属性。 "use strict"; // 建立一个新的 globetrotter(环球者)! const globetrotter = { // globetrotter 如今所处国家所说的语言 const current_lang = undefined, // globetrotter 已近环游过的国家 let countries = 0, // 查看环游过哪些国家了 get countryCount () { return this.countries; }, // 不论 globe trotter 飞到哪里,都从新设置他的语言 set languages (language) { // 增长环游过的城市数 countries += 1; // 重置当前语言 this.current_lang = language; }; }; globetrotter.language = 'Japanese'; globetrotter.countryCount(); // 1 globetrotter.language = 'Spanish'; globetrotter.countryCount(); // 2
上面讲的关于getters的也一样适用于setters,但有一点不一样:
getter不接受参数,setters必须接受正好一个参数。
破坏这些规则中的任意一个都会抛出一个错误。
既然 Angular 2 正在引入TypeCript而且把class带到了台前,我但愿get and set可以流行起来… 但还有点但愿它们不要流行起来。
将来的JavaScript正在变成现实,是时候把它提供的东西都用起来了。这篇文章里,咱们浏览了 ES2015的三个很流行的特性:
let和const带来的块级做用域;
箭头函数带来的this的词法做用域;
简写属性和方法,以及getter和setter函数的回顾。
在本文的开始,咱们要说明一件事:
从本质上说,ES6的classes主要是给建立老式构造函数提供了一种更加方便的语法,并非什么新魔法 —— Axel Rauschmayer,Exploring ES6做者
从功能上来说,class声明就是一个语法糖,它只是比咱们以前一直使用的基于原型的行为委托功能更强大一点。本文将重新语法与原型的关系入手,仔细研究ES2015的class关键字。文中将说起如下内容:
定义与实例化类;
使用extends建立子类;
子类中super语句的调用;
以及重要的标记方法(symbol method)的例子。
在此过程当中,咱们将特别注意 class 声明语法从本质上是如何映射到基于原型代码的。
让咱们从头开始提及。
JavaScript的『类』与Java、Python或者其余你可能用过的面向对象语言中的类不一样。其实后者可能称做面向『类』的语言更为准确一些。
在传统的面向类的语言中,咱们建立的类是对象的模板。须要一个新对象时,咱们实例化这个类,这一步操做告诉语言引擎将这个类的方法和属性复制到一个新实体上,这个实体称做实例。实例是咱们本身的对象,且在实例化以后与父类毫无内在联系。
而JavaScript没有这样的复制机制。在JavaScript中『实例化』一个类建立了一个新对象,但这个新对象却不独立于它的父类。
正相反,它建立了一个与原型相链接的对象。即便是在实例化以后,对于原型的修改也会传递到实例化的新对象去。
原型自己就是一个无比强大的设计模式。有许多使用了原型的技术模仿了传统类的机制,class便为这些技术提供了简洁的语法。
总而言之:
JavaScript不存在Java和其余面向对象语言中的类概念;
JavaScript 的class很大程度上只是原型继承的语法糖,与传统的类继承有很大的不一样。
搞清楚这些以后,让咱们先看一下class。
咱们使用class 关键字建立类,关键字以后是变量标识符,最后是一个称做类主体的代码块。这种写法称做类的声明。没有使用extends关键字的类声明被称做基类:
"use strict"; // Food 是一个基类 class Food { constructor (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F` } print () { console.log( this.toString() ); } } const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.print(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F' console.log(chicken_breast.protein); // 26 (LINE A)
须要注意到如下事情:
类只能包含方法定义,不能有数据属性;
定义方法时,可使用简写方法定义;
与建立对象不一样,咱们不能在类主体中使用逗号分隔方法定义;
咱们能够在实例化对象上直接引用类的属性(如 LINE A)。
类有一个独有的特性,就是 contructor 构造方法。在构造方法中咱们能够初始化对象的属性。
构造方法的定义并非必须的。若是不写构造方法,引擎会为咱们插入一个空的构造方法:
"use strict"; class NoConstructor { /* JavaScript 会插入这样的代码: constructor () { } */ } const nemo = new NoConstructor(); // 能工做,但没啥意思
将一个类赋值给一个变量的形式叫类表达式,这种写法能够替代上面的语法形式:
"use strict"; // 这是一个匿名类表达式,在类主体中咱们不能经过名称引用它 const Food = class { // 和上面同样的类定义…… } // 这是一个命名类表达式,在类主体中咱们能够经过名称引用它 const Food = class FoodClass { // 和上面同样的类定义…… // 添加一个新方法,证实咱们能够经过内部名称引用 FoodClass…… printMacronutrients () { console.log(`${FoodClass.name} | ${FoodClass.protein} g P :: ${FoodClass.carbs} g C :: ${FoodClass.fat} g F`) } } const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.printMacronutrients(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F' // 可是不能在外部引用 try { console.log(FoodClass.protein); // 引用错误 } catch (err) { // pass }
这一行为与匿名函数与命名函数表达式很相似。
使用extends建立的类被称做子类,或派生类。这一用法简单明了,咱们直接在上面的例子中构建:
"use strict"; // FatFreeFood 是一个派生类 class FatFreeFood extends Food { constructor (name, protein, carbs) { super(name, protein, carbs, 0); } print () { super.print(); console.log(`Would you look at that -- ${this.name} has no fat!`); } } const fat_free_yogurt = new FatFreeFood('Greek Yogurt', 16, 12); fat_free_yogurt.print(); // 'Greek Yogurt | 26g P :: 16g C :: 0g F / Would you look at that -- Greek Yogurt has no fat!'
派生类拥有咱们上文讨论的一切有关基类的特性,另外还有以下几点新特色:
子类使用class关键字声明,以后紧跟一个标识符,而后使用extend关键字,最后写一个任意表达式。这个表达式一般来说就是个标识符,但理论上也能够是函数。
若是你的派生类须要引用它的父类,可使用super关键字。
一个派生类不能有一个空的构造函数。即便这个构造函数就是调用了一下super(),你也得把它显式的写出来。但派生类却能够没有构造函数。
在派生类的构造函数中,必须先调用super,才能使用this关键字(译者注:仅在构造函数中是这样,在其余方法中能够直接使用this)。
在JavaScript中仅有两个super关键字的使用场景:
在子类构造函数中调用。若是初始化派生类是须要使用父类的构造函数,咱们能够在子类的构造函数中调用super(parentConstructorParams),传递任意须要的参数。
引用父类的方法。在常规方法定义中,派生类可使用点运算符来引用父类的方法:super.methodName。
咱们的 FatFreeFood 演示了这两种状况:
在构造函数中,咱们简单的调用了super,并将脂肪的量传入为0。
在咱们的print方法中,咱们先调用了super.print,以后才添加了其余的逻辑。
无论你信不信,我反正是信了以上说的已涵盖了有关class的基础语法,这就是你开始实验须要掌握的所有内容。
如今咱们开始关注class是怎么映射到JavaScript内部的原型机制的。咱们会关注如下几点:
使用构造调用建立对象;
原型链接的本质;
属性和方法委托;
使用原型模拟类。
使用构造调用建立对象
构造函数不是什么新鲜玩意儿。使用new关键字调用任意函数会使其返回一个对象 —— 这一步称做建立了一个构造调用,这种函数一般被称做构造器:
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // 使用 'new' 关键字调用 Food 方法,就是构造调用,该操做会返回一个对象 const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); console.log(chicken_breast.protein) // 26 // 不用 'new' 调用 Food 方法,会返回 'undefined' const fish = Food('Halibut', 26, 0, 2); console.log(fish); // 'undefined'
当咱们使用new关键字调用函数时,JS内部执行了下面四个步骤:
建立一个新对象(这里称它为O);
给O赋予一个链接到其余对象的连接,称为原型;
将函数的this引用指向O;
函数隐式返回O。
在第三步和第四步之间,引擎会执行你函数中的具体逻辑。
知道了这一点,咱们就能够重写Food方法,使之不用new关键字也能工做:
"use strict"; // 演示示例:消除对 'new' 关键字的依赖 function Food (name, protein, carbs, fat) { // 第一步:建立新对象 const obj = { }; // 第二步:连接原型——咱们在下文会更加具体地探究原型的概念 Object.setPrototypeOf(obj, Food.prototype); // 第三步:设置 'this' 指向咱们的新对象 // 尽然咱们不能再运行的执行上下文中重置 `this` // 咱们在使用 'obj' 取代 'this' 来模拟第三步 obj.name = name; obj.protein = protein; obj.carbs = carbs; obj.fat = fat; // 第四步:返回新建立的对象 return obj; } const fish = Food('Halibut', 26, 0, 2); console.log(fish.protein); // 26
四步中的三步都是简单明了的。建立一个对象、赋值属性、而后写一个return声明,这些操做对大多数开发者来讲没有理解上的问题——然而这就是难倒众人的黑魔法原型。
在一般状况下,JavaScript中的包括函数在内的全部对象都会连接到另外一个对象上,这就是原型。
若是咱们访问一个对象自己没有的属性,JavaScript就会在对象的原型上检查该属性。换句话说,若是你对一个对象请求它没有的属性,它会对你说:『这个我不知道,问个人原型吧』。
在另外一个对象上查找不存在属性的过程称做委托。
"use strict"; // joe 没有 toString 方法…… const joe = { name : 'Joe' }, sara = { name : 'Sara' }; Object.hasOwnProperty(joe, toString); // false Object.hasOwnProperty(sara, toString); // false // ……但咱们仍是能够调用它! joe.toString(); // '[object Object]',而不是引用错误! sara.toString(); // '[object Object]',而不是引用错误!
尽管咱们的 toString 的输出彻底没啥用,但请注意:这段代码没有引发任何的ReferenceError!这是由于尽管joe和sara没有toString的属性,但他们的原型有啊。
当咱们寻找sara.toString()方法时,sara说:『我没有toString属性,找个人原型吧』。正如上文所说,JavaScript会亲切的询问Object.prototype 是否含有toString属性。因为原型上有这一属性,JS 就会把Object.prototype上的toString返回给咱们程序并执行。
sara自己没有属性不要紧——咱们会把查找操做委托到原型上。
换言之,咱们就能够访问到对象上并不存在的属性,只要其的原型上有这些属性。咱们能够利用这一点将属性和方法赋值到对象的原型上,而后咱们就能够调用这些属性,好像它们真的存在在那个对象上同样。
更给力的是,若是几个对象共享相同的原型——正如上面的joe和sara的例子同样——当咱们给原型赋值属性以后,它们就均可以访问了,无需将这些属性单独拷贝到每个对象上。
这就是为什么你们把它称做原型继承——若是个人对象没有,但对象的原型有,那个人对象也能继承这个属性。
事实上,这里并无发生什么『继承』。在面向类的语言里,继承指从父类复制属性到子类的行为。在JavaScript里,没发生这种复制的操做,事实上这就是原型继承与类继承相比的一个主要优点。
在咱们探究原型到底是怎么来的以前,咱们先作一个简要回顾:
joe和sara没有『继承』一个toString的属性;
joe和sara实际上根本没有从Object.prototype上『继承』;
joe和sara是连接到了Object.prototype上;
joe和sara连接到了同一个Object.prototype上。
若是想找到一个对象的(咱们称它做O)原型,咱们可使用 Object.getPrototypeof(O)。
而后咱们再强调一遍:对象没有『继承自』他们的原型。他们只是委托到原型上。
以上。
接下来让咱们深刻一下。
咱们已了解到基本上每一个对象(下文以O指代)都有原型(下文以P指代),而后当咱们查找O上没有的属性,JavaScript引擎就会在P上寻找这个属性。
至此咱们有两个问题:
以上状况函数怎么玩?
这些原型是从哪里来的?
名为Object的函数
在JavaScript引擎执行程序以前,它会建立一个环境让程序在内部执行,在执行环境中会建立一个函数,叫作Object, 以及一个关联对象,叫作Object.prototype。
换句话说,Object和Object.prototype在任意执行中的JavaScript程序中永远存在。
这个Object乍一看好像和其余函数没什么区别,但特别之处在于它是一个构造器——在调用它时返回一个新对象:
"use strict"; typeof new Object(); // "object" typeof Object(); // 这个 Object 函数的特色是不须要使用 new 关键字调用
这个Object.prototype对象是个……对象。正如其余对象同样,它有属性。
关于Object和Object.prototype你须要知道如下几点:
Object函数有一个叫作.prototype的属性,指向一个对象(Object.prototype);
Object.prototype对象有一个叫作.constructor的属性,指向一个函数(Object)。
实际上,这个整体方案对于JavaScript中的全部函数都是适用的。当咱们建立一个函数——下文称做 someFunction——这个函数就会有一个属性.prototype,指向一个叫作someFunction.prototype 的对象。
与之相反,someFunction.prototype对象会有一个叫作.contructor的属性,它的引用指回函数someFunction。
"use strict"; function foo () { console.log('Foo!'); } console.log(foo.prototype); // 指向一个叫 'foo' 的对象 console.log(foo.prototype.constructor); // 指向 'foo' 函数 foo.prototype.constructor(); // 输出 'Foo!' —— 仅为证实确实有 'foo.prototype.constructor' 这么个方法且指向原函数
须要记住如下几个要点:
全部的函数都有一个属性,叫作 .prototype,它指向这个函数的关联对象。
全部函数的原型都有一个属性,叫作 .constructor,它指向这个函数自己。
一个函数原型的 .constructor 并不是必须指向建立这个函数原型的函数……有点绕,咱们等下会深刻探讨一下。
设置函数的原型有一些规则,在开始以前,咱们先归纳设置对象原型的三个规则:
『默认』规则;
使用new隐式设置原型;
使用Object.create显式设置原型。
考虑下这段代码:
"use strict"; const foo = { status : 'foobar' };
十分简单,咱们作的事儿就是建立一个叫foo的对象,而后给他一个叫status的属性。
而后JavaScript在幕后多作了点工做。当咱们在字面上建立一个对象时,JavaScript将对象的原型指向Object.prototype并设置其原型的.constructor指向Object:
"use strict"; const foo = { status : 'foobar' }; Object.getPrototypeOf(foo) === Object.prototype; // true foo.constructor === Object; // true
让咱们再看下以前调整过的 Food 例子。
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; }
如今咱们知道函数Food将会与一个叫作Food.prototype的对象关联。
当咱们使用new关键字建立一个对象,JavaScript将会:
设置这个对象的原型指向咱们使用new调用的函数的.prototype属性;
设置这个对象的.constructor指向咱们使用new调用到的构造函数。
const tootsie_roll = new Food('Tootsie Roll', 0, 26, 0); Object.getPrototypeOf(tootsie_roll) === Food.prototype; // true tootsie_roll.constructor === Food; // true
这就可让咱们搞出下面这样的黑魔法:
"use strict"; Food.prototype.cook = function cook () { console.log(`${this.name} is cooking!`); }; const dinner = new Food('Lamb Chops', 52, 8, 32); dinner.cook(); // 'Lamb Chops are cooking!'
最后咱们可使用Object.create方法手工设置对象的原型引用。
"use strict"; const foo = { speak () { console.log('Foo!'); } }; const bar = Object.create(foo); bar.speak(); // 'Foo!' Object.getPrototypeOf(bar) === foo; // true
还记得使用new调用函数的时候,JavaScript在幕后干了哪四件事儿吗?Object.create就干了这三件事儿:
建立一个新对象;
设置它的原型引用;
返回这个新对象。
你能够本身去看下MDN上写的那个polyfill。
(译者注:polyfill就是给老代码实现现有新功能的补丁代码,这里就是指老版本JS没有Object.create函数,MDN上有手工撸的一个替代方案)
直接使用原型来模拟面向类的行为须要一些技巧。
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.prototype.toString = function () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; function FatFreeFood (name, protein, carbs) { Food.call(this, name, protein, carbs, 0); } // 设置 "subclass" 关系 // ===================== // LINE A :: 使用 Object.create 手动设置 FatFreeFood's 『父类』. FatFreeFood.prototype = Object.create(Food.prototype); // LINE B :: 手工重置 constructor 的引用 Object.defineProperty(FatFreeFood.constructor, "constructor", { enumerable : false, writeable : true, value : FatFreeFood });
在Line A,咱们须要设置FatFreeFood.prototype使之等于一个新对象,这个新对象的原型引用是Food.prototype。若是没这么搞,咱们的子类就不能访问『超类』的方法。
不幸的是,这个致使了至关诡异的结果:FatFreeFood.constructor是Function,而不是FatFreeFood。为了保证一切正常,咱们须要在Line B手工设置FatFreeFood.constructor。
让开发者从使用原型对类行为笨拙的模仿中脱离苦海是class关键字的产生动机之一。它确实也提供了避免原型语法常见陷阱的解决方案。
如今咱们已经探究了太多关于JavaScript的原型机制,你应该更容易理解class关键字让一切变得多么简单了吧!
如今咱们已了解到JavaScript原型系统的必要性,咱们将深刻探究一下类支持的三种方法,以及一种特殊状况,以结束本文的讨论。
构造器;
静态方法;
原型方法;
一种原型方法的特殊状况:『标记方法』。
并不是我提出的这三组方法,这要归功于Rauschmayer博士在探索ES6一书中的定义。
一个类的constructor方法用于关注咱们的初始化逻辑,constructor方法有如下几个特殊点:
只有在构造方法里,咱们才能够调用父类的构造器;
它在背后处理了全部设置原型链的工做;
它被用做类的定义。
第二点就是在JavaScript中使用class的一个主要好处,咱们来引用一下《探索 ES6》书里的15.2.3.1 的标题:
子类的原型就是超类
正如咱们所见,手工设置很是繁琐且容易出错。若是咱们使用class关键字,JavaScript在内部会负责搞定这些设置,这一点也是使用class的优点。
第三点有点意思。在JavaScript中类仅仅是个函数——它等同于与类中的constructor方法。
"use strict"; class Food { // 和以前同样的类定义…… } typeof Food; // 'function'
与通常把函数做为构造器的方式不一样,咱们不能不用new关键字而直接调用类构造器:
const burrito = Food('Heaven', 100, 100, 25); // 类型错误
这就引起了另外一个问题:当咱们不用new调用函数构造器的时候发生了什么?
简短的回答是:对于任何没有显式返回的函数来讲都是返回undefined。咱们只须要相信用咱们构造函数的用户都会使用构造调用。这就是社区为什么约定构造方法的首字母大写:提醒使用者要用new来调用。
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } const fish = Food('Halibut', 26, 0, 2); // D'oh . . . console.log(fish); // 'undefined'
长一点的回答是:返回undefined,除非你手工检测是否使用被new调用,而后进行本身的处理。
ES2015引入了一个属性使得这种检测变得简单: new.target.
new.target是一个定义在全部使用new调用的函数上的属性,包括类构造器。 当咱们使用new关键字调用函数时,函数体内的new.target的值就是这个函数自己。若是函数没有被new调用,这个值就是undefined。
"use strict"; // 强行构造调用 function Food (name, protein, carbs, fat) { // 若是用户忘了手工调用一下 if (!new.target) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } const fish = Food('Halibut', 26, 0, 2); // 糟了,不过不要紧! fish; // 'Food {name: "Halibut", protein: 20, carbs: 5, fat: 0}'
在ES5里用起来也还行:
"use strict"; function Food (name, protein, carbs, fat) { if (!(this instanceof Food)) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; }
MDN文档讲述了new.target的更多细节,并且给有兴趣者配上了ES2015规范做为参考。规范里有关 [[Construct]] 的描述颇有启发性。
静态方法是构造方法本身的方法,不能被类的实例化对象调用。咱们使用static关键字定义静态方法。
"use strict"; class Food { // 和以前同样…… // 添加静态方法 static describe () { console.log('"Food" 是一种存储了养分信息的数据类型'); } } Food.describe(); // '"Food" 是一种存储了养分信息的数据类型'
静态方法与老式构造函数中直接属性赋值类似:
"use strict"; function Food (name, protein, carbs, fat) { Food.count += 1; this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.count = 0; Food.describe = function count () { console.log(`你建立了 ${Food.count} 个 food`); }; const dummy = new Food(); Food.describe(); // "你建立了 1 个 food"
任何不是构造方法和静态方法的方法都是原型方法。之因此叫原型方法,是由于咱们以前经过给构造函数的原型上附加方法的方式来实现这一功能。
"use strict"; // 使用 ES6: class Food { constructor (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; } print () { console.log( this.toString() ); } } // 在 ES5 里: function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // 『原型方法』的命名大概来自咱们以前经过给构造函数的原型上附加方法的方式来实现这一功能。 Food.prototype.toString = function toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; Food.prototype.print = function print () { console.log( this.toString() ); };
应该说明,在方法定义时彻底可使用生成器。
"use strict"; class Range { constructor(from, to) { this.from = from; this.to = to; } * generate () { let counter = this.from, to = this.to; while (counter < to) { if (counter == to) return counter++; else yield counter++; } } } const range = new Range(0, 3); const gen = range.generate(); for (let val of range.generate()) { console.log(`Generator 的值是 ${ val }. `); // Prints: // Generator 的值是 0. // Generator 的值是 1. // Generator 的值是 2. }
最后咱们说说标志方法。这是一些名为Symbol值的方法,当咱们在自定义对象中使用内置构造器时,JavaScript引擎能够识别并使用这些方法。
MDN文档提供了一个Symbol是什么的简要概览:
Symbol是一个惟一且不变的数据类型,能够做为一个对象的属性标示符。
建立一个新的symbol,会给咱们提供一个被认为是程序里的惟一标识的值。这一点对于命名对象的属性十分有用:咱们能够确保不会不当心覆盖任何属性。使用Symbol作键值也不是无数的,因此他们很大程度上对外界是不可见的(也不彻底是,能够经过Reflect.ownKeys得到)
"use strict"; const secureObject = { // 这个键能够看做是惟一的 [new Symbol("name")] : 'Dr. Secure A. F.' }; console.log( Object.getKeys(superSecureObject) ); // [] -- 标志属性不太好获取 console.log( Reflect.ownKeys(secureObject) ); // [Symbol("name")] -- 但也不是彻底隐藏的
对咱们来说更有意思的是,这给咱们提供了一种方式来告诉 JavaScript 引擎使用特定方法来达到特定的目的。
所谓的『众所周知的Symbol』是一些特定对象的键,当你在定义对象中使用时他们时,JavaScript引擎会触发一些特定方法。
这对于JavaScript来讲有点怪异,咱们仍是看个例子吧:
"use strict"; // 继承 Array 可让咱们直观的使用 'length' // 同时可让咱们访问到内置方法,如 // map、filter、reduce、push、pop 等 class FoodSet extends Array { // foods 把传递的任意参数收集为一个数组 // 参见:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator constructor(...foods) { super(); this.foods = []; foods.forEach((food) => this.foods.push(food)) } // 自定义迭代器行为,请注意,这不是多么好用的迭代器,可是个不错的例子 // 键名前必须写星号 * [Symbol.iterator] () { let position = 0; while (position < this.foods.length) { if (position === this.foods.length) { return "Done!" } else { yield `${this.foods[ position++ ]} is the food item at position ${position}`; } } } // 当咱们的用户使用内置的数组方法,返回一个数组类型对象 // 而不是 FoodSet 类型的。这使得咱们的 FoodSet 能够被一些 // 指望操做数组的代码操做 static get [Symbol.species] () { return Array; } } const foodset = new FoodSet(new Food('Fish', 26, 0, 16), new Food('Hamburger', 26, 48, 24)); // 当咱们使用 for ... of 操做 FoodSet 时,JavaScript 将会使用 // 咱们以前用 [Symbol.iterator] 作键值的方法 for (let food of foodset) { // 打印所有 food console.log( food ); } // 当咱们执行数组的 `filter` 方法时,JavaScript 建立并返回一个新对象 // 咱们在什么对象上执行 `filter` 方法,新对象就使用这个对象做为默认构造器来建立 // 然而大部分代码都但愿 filter 返回一个数组,因而咱们经过重写 [Symbol.species] // 的方式告诉 JavaScript 使用数组的构造器 const healthy_foods = foodset.filter((food) => food.name !== 'Hamburger'); console.log( healthy_foods instanceof FoodSet ); // console.log( healthy_foods instanceof Array );
当你使用for...of遍历一个对象时,JavaScript将会尝试执行对象的迭代器方法,这一方法就是该对象 Symbol.iterator属性上关联的方法。若是咱们提供了本身的方法定义,JavaScript就会使用咱们自定义的。若是没有本身制定的话,若是有默认的实现就用默认的,没有的话就不执行。
Symbo.species更奇异了。在自定义的类中,默认的Symbol.species函数就是类的构造函数。当咱们的子类有内置的集合(例如Array和Set)时,咱们一般但愿在使用父类的实例时也能使用子类。
经过方法返回父类的实例而不是派生类的实例,使咱们更能确保咱们子类在大多数代码里的可用性。而Symbol.species能够实现这一功能。
若是不怎么须要这个功能就别费力去搞了。Symbol的这种用法——或者说有关Symbol的所有用法——都还比较罕见。这些例子只是为了演示:
咱们能够在自定义类中使用JavaScript内置的特定构造器;
用两个普通的例子展现了怎么实现这一点。
ES2015的class关键字没有带给咱们 Java 里或是SmallTalk里那种『真正的类』。宁肯说它只是提供了一种更加方便的语法来建立经过原型关联的对象,本质上没有什么新东西。
ES2015发生了一些重大变革,像promises和generators. 但并不是新标准的一切都遥不可及。 – 至关一部分新特性能够快速上手。
在这篇文章里,咱们来看下新特性带来的好处:
新的集合: map,weakmap,set, weakset
大部分的new String methods
模板字符串。
咱们开始这个系列的最后一章吧。
模板字符串 解决了三个痛点,容许你作以下操做:
定义在字符串内部的表达式,称为 字符串插值。
写多行字符串无须用换行符 (n) 拼接。
使用“raw”字符串 – 在反斜杠内的字符串不会被转义,视为常量。
“use strict”; /* 三个模板字符串的例子: 字符串插值,多行字符串,raw 字符串。 ================================= */ // ================================== // 1. 字符串插值 :: 解析任何一个字符串中的表达式。 console.log(1 + 1 = ${1 + 1}); // ================================== // 2. 多行字符串 :: 这样写: let childe_roland = I saw them and I knew them all. And yet <br> Dauntless the slug-horn to my lips I set, <br> And blew “Childe Roland to the Dark Tower came.” // … 代替下面的写法: child_roland = ‘I saw them and I knew them all. And yet\n’ + ‘Dauntless the slug-horn to my lips I set,\n’ + ‘And blew “Childe Roland to the Dark Tower came.”’; // ================================== // 3. raw 字符串 :: 在字符串前加 raw 前缀,javascript 会忽略转义字符。 // 依然会解析包在 ${} 的表达式 const unescaped = String.rawThis ${string()} doesn't contain a newline!\n function string () { return “string”; } console.log(unescaped); // ‘This string doesn’t contain a newline!\n’ – 注意 \n 会被原样输出 // 你能够像 React 使用 JSX 同样,用模板字符串建立 HTML 模板 const template = ` Example I’m a pure JS & HTML template! ` function getClass () { // Check application state, calculate a class based on that state return “some-stateful-class”; } console.log(template); // 这样使用略显笨,本身试试吧! // 另外一个经常使用的例子是打印变量名: const user = { name : ‘Joe’ }; console.log(“User’s name is ” + user.name + “.”); // 有点冗长 console.log(User's name is ${user.name}.); // 这样稍好一些
使用字符串插值,用反引号代替引号包裹字符串,并把咱们想要的表达式嵌入在${}中。
对于多行字符串,只须要把你要写的字符串包裹在反引号里,在要换行的地方直接换行。 JavaScript 会在换行处插入新行。
使用原生字符串,在模板字符串前加前缀String.raw,仍然使用反引号包裹字符串。
模板字符串或许只不过是一种语法糖 … 但它比语法糖略胜一筹。
ES2015也给String新增了一些方法。他们主要归为两类:
通用的便捷方法
扩充 Unicode 支持的方法。
在本文里咱们只讲第一类,同时unicode特定方法也有至关好的用例 。若是你感兴趣的话,这是地址在MDN的文档里,有一个关于字符串新方法的完整列表。
对新手而言,咱们有String.prototype.startsWith。 它对任何字符串都有效,它须要两个参数:
一个是 search string 还有
整形的位置参数 n。这是可选的。
String.prototype.startsWith方法会检查以nth位起的字符串是否以search string开始。若是没有位置参数,则默认从头开始。
若是字符串以要搜索的字符串开头返回 true,不然返回 false。
"use strict"; const contrived_example = "This is one impressively contrived example!"; // 这个字符串是以 "This is one" 开头吗? console.log(contrived_example.startsWith("This is one")); // true // 这个字符串的第四个字符以 "is" 开头? console.log(contrived_example.startsWith("is", 4)); // false // 这个字符串的第五个字符以 "is" 开始? console.log(contrived_example.startsWith("is", 5)); // true
String.prototype.endsWith和startswith类似: 它也须要两个参数:一个是要搜索的字符串,一个是位置。
然而String.prototype.endsWith位置参数会告诉函数要搜索的字符串在原始字符串中被当作结尾处理。
换句话说,它会切掉nth后的全部字符串,并检查是否以要搜索的字符结尾。
"use strict"; const contrived_example = "This is one impressively contrived example!"; console.log(contrived_example.endsWith("contrived example!")); // true console.log(contrived_example.slice(0, 11)); // "This is one" console.log(contrived_example.endsWith("one", 11)); // true // 一般状况下,传一个位置参数向下面这样: function substringEndsWith (string, search_string, position) { // Chop off the end of the string const substring = string.slice(0, position); // 检查被截取的字符串是否已 search_string 结尾 return substring.endsWith(search_string); }
ES2015也添加了String.prototype.includes。 你须要用字符串调用它,而且要传递一个搜索项。若是字符串包含搜索项会返回true,反之返回false。
"use strict"; const contrived_example = "This is one impressively contrived example!"; // 这个字符串是否包含单词 impressively ? contrived_example.includes("impressively"); // true
ES2015以前,咱们只能这样:
"use strict"; contrived_example.indexOf("impressively") !== -1 // true
不算太坏。可是,String.prototype.includes是 一个改善,它屏蔽了任意整数返回值为true的漏洞。
还有String.prototype.repeat。能够对任意字符串使用,像includes同样,它会或多或少地完成函数名指示的工做。
它只须要一个参数: 一个整型的count。使用案例说明一切,上代码:
const na = "na"; console.log(na.repeat(5) + ", Batman!"); // 'nanananana, Batman!'
最后,咱们有String.raw,咱们在上面简单介绍过。
一个模板字符串以 String.raw 为前缀,它将不会在字符串中转义:
/* 单右斜线要转义,咱们须要双右斜线才能打印一个右斜线,\n 在普通字符串里会被解析为换行 * */ console.log('This string \\ has fewer \\ backslashes \\ and \n breaks the line.'); // 不想这样写的话用 raw 字符串 String.raw`This string \\ has too many \\ backslashes \\ and \n doesn't break the line.`
虽然咱们不涉及剩余的 string 方法,可是若是我不告诉你去这个主题的必读部分就会显得我疏忽。
Dr Rauschmayer对于Unicode in JavaScript的介绍
他关于ES2015’s Unicode Support in Exploring ES6和The Absolute Minimum Every Software Developer Needs to Know About Unicode 的讨论。
不管如何我不得不跳过它的最后一部分。虽然有些老可是仍是有优势的。
这里是文档中缺失的字符串方法,这样你会知道缺哪些东西了。
String.fromCodePoint & String.prototype.codePointAt;
String.prototype.normalize;
Unicode point escapes.
ES2015新增了一些集合类型:
Map和WeakMap
Set和WeakSet。
合适的Map和Set类型十分方便使用,还有弱变量是一个使人兴奋的改动,虽然它对Javascript来讲像舶来品同样。
map就是简单的键值对。最简单的理解方式就是和object相似,一个键对应一个值。
"use strict"; // 咱们能够把 foo 当键,bar 当值 const obj = { foo : 'bar' }; // 对象键为 foo 的值为 bar obj.foo === 'bar'; // true
新的Map类型在概念上是类似的,可是可使用任意的数据类型做为键 – 不止strings和symbols–还有除了pitfalls associated with trying to use an objects a map的一些东西。
下面的片断例举了 Map 的 API.
"use strict"; // 构造器 let scotch_inventory = new Map(); // BASIC API METHODS // Map.prototype.set (K, V) :: 建立一个键 K,并设置它的值为 V。 scotch_inventory.set('Lagavulin 18', 2); scotch_inventory.set('The Dalmore', 1); // 你能够建立一个 map 里面包含一个有两个元素的数组 scotch_inventory = new Map([['Lagavulin 18', 2], ['The Dalmore', 1]]); // 全部的 map 都有 size 属性,这个属性会告诉你 map 里有多少个键值对。 // 用 Map 或 Set 的时候,必定要使用 size ,不能使用 length console.log(scotch_inventory.size); // 2 // Map.prototype.get(K) :: 返回键相关的值。若是键不存在返回 undefined console.log(scotch_inventory.get('The Dalmore')); // 1 console.log(scotch_inventory.get('Glenfiddich 18')); // undefined // Map.prototype.has(K) :: 若是 map 里包含键 K 返回true,不然返回 false console.log(scotch_inventory.has('The Dalmore')); // true console.log(scotch_inventory.has('Glenfiddich 18')); // false // Map.prototype.delete(K) :: 从 map 里删除键 K。成功返回true,不存在返回 false console.log(scotch_inventory.delete('The Dalmore')); // true -- breaks my heart // Map.prototype.clear() :: 清楚 map 中的全部键值对 scotch_inventory.clear(); console.log( scotch_inventory ); // Map {} -- long night // 遍历方法 // Map 提供了多种方法遍历键值。 // 重置值,继续探索 scotch_inventory.set('Lagavulin 18', 1); scotch_inventory.set('Glenfiddich 18', 1); /* Map.prototype.forEach(callback[, thisArg]) :: 对 map 里的每一个键值对执行一个回调函数 * 你能够在回调函数内部设置 'this' 的值,经过传递一个 thisArg 参数,那是可选的并且没有太大必要那样作 * 最后,注意回调函数已经被传了键和值 */ scotch_inventory.forEach(function (quantity, scotch) { console.log(`Excuse me while I sip this ${scotch}.`); }); // Map.prototype.keys() :: 返回一个 map 中的全部键 const scotch_names = scotch_inventory.keys(); for (let name of scotch_names) { console.log(`We've got ${name} in the cellar.`); } // Map.prototype.values() :: 返回 map 中的全部值 const quantities = scotch_inventory.values(); for (let quantity of quantities) { console.log(`I just drank ${quantity} of . . . Uh . . . I forget`); } // Map.prototype.entries() :: 返回 map 的全部键值对,提供一个包含两个元素的数组 // 之后会常常看到 map 里的键值对和 "entries" 关联 const entries = scotch_inventory.entries(); for (let entry of entries) { console.log(`I remember! I drank ${entry[1]} bottle of ${entry[0]}!`); }
可是Object在保存键值对的时候仍然有用。 若是符合下面的所有条件,你可能仍是想用Object:
当你写代码的时候,你知道你的键值对。
你知道你可能不会去增长或删除你的键值对。
你使用的键全都是 string 或 symbol。
另外一方面,若是符合如下任意条件,你可能会想使用一个 map。
你须要遍历整个map – 然而这对 object 来讲是难以置信的.
当你写代码的时候不须要知道键的名字或数量。
你须要复杂的键,像 Object 或 别的 Map (!).
像遍历一个map同样遍历一个object是可行的,但奇妙的是–还会有一些坑潜伏在暗处。 Map更容易使用,而且增长了一些可集成的优点。然而object是以随机顺序遍历的,map是以插入的顺序遍历的。
添加随意动态键名的键值对给一个object是可行的。但奇妙的是: 好比说若是你曾经遍历过一个伪 map,你须要记住手动更新条目数。
最后一条,若是你要设置的键名不是string或symbol,你除了选择Map别无选择。
上面的这些只是一些指导性的意见,并非最好的规则。
你可能据说过一个特别棒的特性垃圾回收器,它会按期地检查再也不使用的对象并清除。
To quote Dr Rauschmayer:
WeakMap 不会阻止它的键值被垃圾回收。那意味着你能够把数据和对象关联起来不用担忧内存泄漏。
换句换说,就是你的程序丢掉了WeakMap键的全部外部引用,他能自动垃圾回收他们的值。
尽管大大简化了用例,考虑到SPA(单页面应用) 就是用来展现用户但愿展现的东西,像一些物品描述和一张图片,咱们能够理解为API返回的JSON。
理论上来讲咱们能够经过缓存响应结果来减小请求服务器的次数。咱们能够这样用Map :
"use strict"; const cache = new Map(); function put (element, result) { cache.set(element, result); } function retrieve (element) { return cache.get(element); }
… 这是行得通的,可是有内存泄漏的危险。
由于这是一个SPA,用户或许想离开这个视图,这样的话咱们的 “视图”object就会失效,会被垃圾回收。
不幸的是,若是你使用的是正常的Map ,当这些object不使用时,你必须自行清除。
使用WeakMap替代就能够解决上面的问题:
"use strict"; const cache = new WeakMap(); // 不会再有内存泄露了 // 剩下的都同样
这样当应用失去不须要的元素的引用时,垃圾回收系统能够自动重用那些元素。
WeakMap的API和Map类似,但有以下几点不一样:
在WeakMap里你可使用object做为键。 这意味着不能以String和Symbol作键。
WeakMap只有set,get,has,和delete方法 – 那意味着你不能遍历weak map.
WeakMaps没有size属性。
不能遍历或检查WeakMap的长度的缘由是,在遍历过程当中可能会遇到垃圾回收系统的运行: 这一瞬间是满的,下一秒就没了。
这种不可预测的行为须要谨慎对待,TC39(ECMA第39届技术委员会)曾试图避免禁止WeakMap的遍历和长度检测。
其余的案例,能够在这里找到Use Cases for WeakMap,来自Exploring ES6.
Set就是只包含一个值的集合。换句换说,每一个set的元素只会出现一次。
这是一个有用的数据类型,若是你要追踪惟一而且固定的object ,好比说聊天室的当前用户。
Set和Map有彻底相同的API。主要的不一样是Set没有set方法,由于它不能存储键值对。剩下的几乎相同。
"use strict"; // 构造器 let scotch_collection = new Set(); // 基本的 API 方法 // Set.prototype.add (O) :: 和 set 同样,添加一个对象 scotch_collection.add('Lagavulin 18'); scotch_collection.add('The Dalmore'); // 你也能够用数组构造一个 set scotch_collection = new Set(['Lagavulin 18', 'The Dalmore']); // 全部的 set 都有一个 length 属性。这个属性会告诉你 set 里有多少对象 // 用 set 或 map 的时候,必定记住用 size,不用 length console.log(scotch_collection.size); // 2 // Set.prototype.has(O) :: 包含对象 O 返回 true 不然返回 false console.log(scotch_collection.has('The Dalmore')); // true console.log(scotch_collection.has('Glenfiddich 18')); // false // Set.prototype.delete(O) :: 删除 set 中的 O 对象,成功返回 true,不存在返回 false scotch_collection.delete('The Dalmore'); // true -- break my heart // Set.prototype.clear() :: 删除 set 中的全部对象 scotch_collection.clear(); console.log( scotch_collection ); // Set {} -- long night. /* 迭代方法 * Set 提供了多种方法遍历 * 从新设置值,继续探索 */ scotch_collection.add('Lagavulin 18'); scotch_collection.add('Glenfiddich 18'); /* Set.prototype.forEach(callback[, thisArg]) :: 执行一个函数,回调函数 * set 里在每一个的键值对。 You can set the value of 'this' inside * the callback by passing a thisArg, but that's optional and seldom necessary. */ scotch_collection.forEach(function (scotch) { console.log(`Excuse me while I sip this ${scotch}.`); }); // Set.prototype.values() :: 返回 set 中的全部值 let scotch_names = scotch_collection.values(); for (let name of scotch_names) { console.log(`I just drank ${name} . . . I think.`); } // Set.prototype.keys() :: 对 set 来讲,和 Set.prototype.values() 方法一致 scotch_names = scotch_collection.keys(); for (let name of scotch_names) { console.log(`I just drank ${name} . . . I think.`); } /* Set.prototype.entries() :: 返回 map 的全部键值对,提供一个包含两个元素的数组 * 这有点多余,可是这种方法能够保留 map API 的可操做性 * */ const entries = scotch_collection.entries(); for (let entry of entries) { console.log(`I got some ${entry[0]} in my cup and more ${entry[1]} in my flask!`); }
WeakSet相对于Set就像WeakMap相对于 Map :
在WeakSet里object的引用是弱类型的。
WeakSet没有property属性。
不能遍历WeakSet。
Weak set的用例并很少,可是这儿有一些Domenic Denicola称呼它们为“perfect for branding” – 意思就是标记一个对象以知足其余需求。
这儿是他给的例子:
/* 下面这个例子来自 Weakset 使用案例的邮件讨论 * 邮件的内容和讨论的其他部分在这儿: * https://mail.mozilla.org/pipermail/es-discuss/2015-June/043027.html */ const foos = new WeakSet(); class Foo { constructor() { foos.add(this); } method() { if (!foos.has(this)) { throw new TypeError("Foo.prototype.method called on an incompatible object!"); } } }
这是一个轻量科学的方法防止你们在一个没有被Foo构造出的object上使用method。
使用的WeakSet的优点是容许foo里的object使用完后被垃圾回收。
这篇文章里,咱们已经了解了ES2015带来的一些好处,从string的便捷方法和模板变量到适当的Map和Set实现。
String方法和模板字符串易于上手。同时你很快也就不用处处用weak set了,我认为你很快就会喜欢上Set和Map。