Part1 · JavaScript【深度剖析】
ES 新特性与 TypeScript、JS 性能优化
文章说明:本专栏内容为本人参加【拉钩大前端高新训练营】的学习笔记以及思考总结,学徒之心,仅为分享。如如有误,请在评论区支出,若是您以为专栏内容还不错,请点赞、关注、评论。前端
共同进步!express
上一篇:【ECMAScript概述】、【ES6概述】、【var let const】、【数组与对象的解构】编程
今天内容有点多,不过大多数都是API用法,只是比较细碎,慢慢嚼,多使用,在使用的过程当中加深理解。数组
7、ES2015模板字符串
1.模板字面量
ECMAScript 6 新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不一样,模板字面量保留换行字符,能够跨行定义字符串:性能优化
使用键盘区域Esc下方【数字1左边】的反引号数据结构
let myMultiLineString = 'first line\nsecond line'; let myMultiLineTemplateLiteral = `first line second line`; console.log(myMultiLineString); // first line // second line" console.log(myMultiLineTemplateLiteral); // first line // second line console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
因此说,当咱们须要写HTML模板时,这个方法很是有用:app
let pageHTML = ` <div> <a href="#"> <span>Jake</span> </a> </div>`;
因为模板字面量会保持反引号内部的空格,所以在使用时要格外注意。格式正确的模板字符串看起来可能会缩进不当。dom
2.字符串插值
模板字面量最经常使用的一个特性是支持字符串插值,也就是能够在一个连续定义中插入一个或多个值。技术上讲,**模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,只不过求值后获得的是字符串。**模板字面量在定义时当即求值并转换为字符串实例,任何插入的变量也会从它们最接近的做用域中取值。ide
字符串插值经过在${}中使用一个 JavaScript 表达式实现:函数
let value = 5; let exponent = 'second'; // 之前,字符串插值是这样实现的: let interpolatedString = value + ' to the ' + exponent + ' power is ' + (value * value); // 如今,能够用模板字面量这样实现: let interpolatedTemplateLiteral = `${ value } to the ${ exponent } power is ${ value * value }`; console.log(interpolatedString); // 5 to the second power is 25 console.log(interpolatedTemplateLiteral); // 5 to the second power is 25 // 全部插入的值都会使用 toString()强制转型为字符串,并且任何 JavaScript 表达式均可以用于插值。
3.模板字面量标签函数
模板字面量也支持定义标签函数(tag function),而经过标签函数能够自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每一个表达式求值的结果。
标签函数自己是一个常规函数,经过前缀到模板字面量来应用自定义行为,以下例所示。标签函数接收到的参数依次是原始字符串数组和对每一个表达式求值的结果。这个函数的返回值是对模板字面量求值获得的字符串。
这样概念解释很不清楚,经过下方的例子来增强理解:
let a = 6; let b = 9; function simpleTag(strings, ...expressions) { console.log(strings); for(const expression of expressions) { console.log(expression); } return 'foobar'; } let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`; // ["", " + ", " = ", ""] // 6 // 9 // 15 console.log(taggedResult); // "foobar"
8、ES2015参数默认值
在 ECMAScript5.1 及之前,实现默认参数的一种经常使用方式就是检测某个参数是否等于 undefined,若是是则意味着没有传这个参数,那就给它赋一个值:
function makeKing(name) { name = (typeof name !== 'undefined') ? name : 'Henry'; return `King ${ name} VIII`; } console.log(makeKing()); // 'King Henry VIII' console.log(makeKing('Louis')); // 'King Louis VIII'
ECMAScript 6 以后就不用这么麻烦了,由于它支持显式定义默认参数了。下面就是与前面代码等价的 ES6 写法,只要在函数定义中的参数后面用=就能够为参数赋一个默认值:
function makeKing(name = 'Henry') { return `King ${ name} VIII`; } console.log(makeKing('Louis')); // 'King Louis VIII' console.log(makeKing()); // 'King Henry VIII'
上面给参数传 undefined 至关于没有传值,不过这样能够利用多个独立的默认值:
function makeKing(name = 'Henry', numerals = 'VIII') { return `King ${ name} ${ numerals}`; } console.log(makeKing()); // 'King Henry VIII' console.log(makeKing('Louis')); // 'King Louis VIII' console.log(makeKing(undefined, 'VI')); // 'King Henry VI'
在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。固然,跟 ES5 严格模式同样,修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准:
function makeKing(name = 'Henry') { name = 'Louis'; return `King ${ arguments[0]}`; } console.log(makeKing()); // 'King undefined' console.log(makeKing('Louis')); // 'King Louis'
默认参数值并不限于原始值或对象类型,也可使用调用函数返回的值:
let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI']; let ordinality = 0; function getNumerals() { // 每次调用后递增 return romanNumerals[ordinality++]; } function makeKing(name = 'Henry', numerals = getNumerals()) { return `King ${ name} ${ numerals}`; } console.log(makeKing()); // 'King Henry I' console.log(makeKing('Louis', 'XVI')); // 'King Louis XVI' console.log(makeKing()); // 'King Henry II' console.log(makeKing()); // 'King Henry III'
函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。并且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用。
箭头函数一样也能够这样使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了:
let makeKing = (name = 'Henry') => `King ${ name}`; console.log(makeKing()); // King Henry
9、ES2015展开数组(Spread)
在 ECMAScript5.1 及之前,咱们从打印出数组中的元素很麻烦,最笨的办法是:
const arr = ['foo', 'bar', 'baz']; console.log( arr[0], arr[1], arr[2] ) // foo bar baz
可是当数组的个数不肯定是,就不能使用这个方法,而且这个方法属于硬展,咱们可使用函数的apply方法,第一个参数this指向console对象,第二个参数是要传递的数组对象:
const arr = ['foo', 'bar', 'baz']; console.log.apply(console, arr) // foo bar baz
在ES6中,咱们能够更简单的使用数组展开的方法,形式同与收集剩余参数,使用…arr展开数组,这样写起来很是方便:
const arr = ['foo', 'bar', 'baz']; console.log(...arr) // foo bar baz
10、ES2015箭头函数
ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式建立的函数对象行为是相同的。任何可使用函数表达式的地方,均可以使用箭头函数:
let arrowSum = (a, b) => { return a + b; }; let functionExpressionSum = function(a, b) { return a + b; }; console.log(arrowSum(5, 8)); // 13 console.log(functionExpressionSum(5, 8)); // 13
若是只有一个参数,那也能够不用括号。只有没有参数,或者多个参数的状况下,才须要使用括号:
// 如下两种写法都有效 let double = (x) => { return 2 * x; }; let triple = x => { return 3 * x; }; // 没有参数须要括号 let getRandom = () => { return Math.random(); }; // 多个参数须要括号 let sum = (a, b) => { return a + b; }; // 无效的写法: let multiply = a, b => { return a * b; };
箭头函数也能够不用大括号,但这样会改变函数的行为。使用大括号就说明包含“函数体”,能够在一个函数中包含多条语句,跟常规的函数同样。若是不使用大括号,那么箭头后面就只能有一行代码,好比一个赋值操做,或者一个表达式。并且,省略大括号会隐式返回这行代码的值:
// 如下两种写法都有效,并且返回相应的值 let double = (x) => { return 2 * x; }; let triple = (x) => 3 * x; // 能够赋值 let value = { }; let setName = (x) => x.name = "Matt"; setName(value); console.log(value.name); // "Matt" // 无效的写法: let multiply = (a, b) => return a * b;
箭头函数虽然语法简洁,但也有不少场合不适用。箭头函数不能使用 arguments、super 和new.target,也不能用做构造函数。此外,箭头函数也没有 prototype 属性。
另外,箭头函数中的this指向也与普通函数不一样,请参考一篇文章:
11、ES2015对象
1.对象字面量的加强
ES6中加强了对象的字面量,在以前,咱们声明对象只能使用键+冒号+值的方式:
let bar = '345' const obj = { name: 'leo', age: '26', bar: bar }
而在ES6中,当咱们的属性名与其值都为变量且相同时,咱们能够省略冒号以及后面的值:
let bar = '345' const obj = { name: 'leo', age: '26', bar }
一样,当咱们须要使用动态的属性名时,以前的作法是在对象声明事后,再对对象赋值动态属性名的值:
const obj = { name: 'leo', age: '26', bar } obj[Math.random()] = 'random'
在ES6中,咱们能够直接使用方括号+表达式对对象添加动态的属性名,这种方式称之为【计算属性名】,方括号内部能够为任意表达式,表达式结果做为最终的属性名:
const obj = { name: 'leo', age: '26', bar, [Math.random()]: 'random' }
2.Object扩展方法
- Object.assign
此方法能够将多个源对象中的属性复制到一个目标对象中,若是对象之间有相同的属性名,那么源对象中的属性就会覆盖掉目标对象中的属性。源对象与目标对象都是普通的对象,只不过用处不一样
const source1 = { a: 123, b: 123 } const source2 = { b: 789, d: 789 } const target = { a: 456, c: 456 } const result = Object.assign(target, source1, source2) console.log(target) console.log(result === target) // { a: 123, c: 456, b: 789, d: 789 } // true
应用场景:
function func (obj) { // obj.name = 'func obj' // console.log(obj) const funcObj = Object.assign({ }, obj) funcObj.name = 'func obj' console.log(funcObj) } const obj = { name: 'global obj' } func(obj) console.log(obj)
assign方法多用于options对象参数设置默认值。
- Object.is
用来判断两个值是否相等,在以前咱们使用和=分别判断值是否相等以及是否全等(值与类型都相等),在==中,js默认使用toString方法来进行隐式转换,而在ES6中,提供了全新的方法Object.is方法进行判断:
console.log( // 0 == false // => true // 0 === false // => false // +0 === -0 // => true // NaN === NaN // => false // Object.is(+0, -0) // => false // Object.is(NaN, NaN) // => true )
在实际使用中,仍然建议使用===来判断。
12、ES2015 Proxy
在 ES6 以前,ECMAScript 中并无相似代理的特性。因为代理是一种新的基础性语言能力,不少转译程序都不能把代理行为转换为以前的 ECMAScript 代码,由于代理的行为其实是无可替代的。为此,代理和反射只在百分之百支持它们的平台上有用。能够检测代理是否存在,不存在则提供后备代码。不过这会致使代码冗余,所以并不推荐。
1.空代理
最简单的代理是空代理,即除了做为一个抽象的目标对象,什么也不作。默认状况下,在代理对象上执行的全部操做都会无障碍地传播到目标对象。所以,在任何可使用目标对象的地方,均可以经过一样的方式来使用与之关联的代理对象。
以下面的代码所示,在代理对象上执行的任何操做实际上都会应用到目标对象。惟一可感知的不一样就是代码中操做的是代理对象。
const target = { id: 'target' }; const handler = { }; const proxy = new Proxy(target, handler); // id 属性会访问同一个值 console.log(target.id); // target console.log(proxy.id); // target // 给目标属性赋值会反映在两个对象上 // 由于两个对象访问的是同一个值 target.id = 'foo'; console.log(target.id); // foo console.log(proxy.id); // foo // 给代理属性赋值会反映在两个对象上 // 由于这个赋值会转移到目标对象 proxy.id = 'bar'; console.log(target.id); // bar console.log(proxy.id); // bar // hasOwnProperty()方法在两个地方 // 都会应用到目标对象 console.log(target.hasOwnProperty('id')); // true console.log(proxy.hasOwnProperty('id')); // true // Proxy.prototype 是 undefined // 所以不能使用 instanceof 操做符 console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check // 严格相等能够用来区分代理和目标 console.log(target === proxy); // false
2.定义捕获器:
使用代理的主要目的是能够定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操做的拦截器”。每一个处理程序对象能够包含零个或多个捕获器,每一个捕获器都对应一种基本操做,能够直接或间接在代理对象上调用。每次在代理对象上调用这些基本操做时,代理能够在这些操做传播到目标对象以前先调用捕获器函数,从而拦截并修改相应的行为。
例如,能够定义一个 get()捕获器,在 ECMAScript 操做以某种形式调用 get()时触发。下面的例子定义了一个 get()捕获器:
const target = { foo: 'bar' }; const handler = { // 捕获器在处理程序对象中以方法名为键 get() { return 'handler override'; } }; const proxy = new Proxy(target, handler); console.log(target.foo); // bar console.log(proxy.foo); // handler override console.log(target['foo']); // bar console.log(proxy['foo']); // handler override console.log(Object.create(target)['foo']); // bar console.log(Object.create(proxy)['foo']); // handler override
捕获器能够定义get、delete、set等方法。
十3、ES2015 class类
ECMAScript 6 新引入的 class 关键字具备正式定义类的能力。类(class)是ECMAScript 中新的基础性语法糖结构,所以刚开始接触时可能会不太习惯。虽然 ECMAScript 6 类表面上看起来能够支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
1.类的定义
与函数类型类似,定义类也有两种主要方式:类声明和类表达式。这两种方式都使用 class 关键字加大括号:
// 类声明 class Person { } // 类表达式 const Animal = class { };
类能够包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。空的类定义照样有效。默认状况下,类定义中的代码都在严格模式下执行。
与函数构造函数同样,多数编程风格都建议类名的首字母要大写,以区别于经过它建立的实例(好比,经过 class Foo {}建立实例 foo):
// 空类定义,有效 class Foo { } // 有构造函数的类,有效 class Bar { constructor() { } } // 有获取函数的类,有效 class Baz { get myBaz() { } } // 有静态方法的类,有效 class Qux { static myQux() { } }
2.类构造函数
constructor 关键字用于在类定义块内部建立类的构造函数。方法名 constructor 会告诉解释器在使用 new 操做符建立类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义构造函数至关于将构造函数定义为空函数。
使用 new 操做符实例化 Person 的操做等于使用 new 调用其构造函数。惟一可感知的不一样之处就是,JavaScript 解释器知道使用 new 和类意味着应该使用 constructor 函数进行实例化。
使用 new 调用类的构造函数会执行以下操做。
-
在内存中建立一个新对象。
-
这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
-
构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
-
执行构造函数内部的代码(给新对象添加属性)。
-
若是构造函数返回非空对象,则返回该对象;不然,返回刚建立的新对象。
class Animal { } class Person { constructor() { console.log('person ctor'); } } class Vegetable { constructor() { this.color = 'orange'; } } let a = new Animal(); let p = new Person(); // person ctor let v = new Vegetable(); console.log(v.color); // orange
3.实例、原型、类成员
类的语法能够很是方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类自己的成员。
每次经过new调用类标识符时,都会执行类构造函数。在这个函数内部,能够为新建立的实例(this)添加“自有”属性。至于添加什么样的属性,则没有限制。另外,在构造函数执行完毕后,仍然能够给实例继续添加新成员。
每一个实例都对应一个惟一的成员对象,这意味着全部成员都不会在原型上共享:
class Person { constructor() { // 这个例子先使用对象包装类型定义一个字符串 // 为的是在下面测试两个对象的相等性 this.name = new String('Jack'); this.sayName = () => console.log(this.name); this.nicknames = ['Jake', 'J-Dog'] } } let p1 = new Person(), p2 = new Person(); p1.sayName(); // Jack p2.sayName(); // Jack console.log(p1.name === p2.name); // false console.log(p1.sayName === p2.sayName); // false console.log(p1.nicknames === p2.nicknames); // false p1.name = p1.nicknames[0]; p2.name = p2.nicknames[1]; p1.sayName(); // Jake p2.sayName(); // J-Dog
静态类成员在类定义中使用 static 关键字做为前缀。在静态成员中,this 引用类自身。其余全部约定跟原型成员同样:
class Person { constructor() { // 添加到 this 的全部内容都会存在于不一样的实例上 this.locate = () => console.log('instance', this); } // 定义在类的原型对象上 locate() { console.log('prototype', this); } // 定义在类自己上 static locate() { console.log('class', this); } } let p = new Person(); p.locate(); // instance, Person {} Person.prototype.locate(); // prototype, {constructor: ... } Person.locate(); // class, class Person {}
4.继承
ES6 类支持单继承。使用 extends 关键字,就能够继承任何拥有[[Construct]]和原型的对象。很大程度上,这意味着不只能够继承一个类,也能够继承普通的构造函数(保持向后兼容):
class Vehicle { } // 继承类 class Bus extends Vehicle { } let b = new Bus(); console.log(b instanceof Bus); // true console.log(b instanceof Vehicle); // true function Person() { } // 继承普通构造函数 class Engineer extends Person { } let e = new Engineer(); console.log(e instanceof Engineer); // true console.log(e instanceof Person); // true
十4、ES2015 Set数据结构
ECMAScript 6 新增的 Set 是一种新集合类型,为这门语言带来集合数据结构。Set 在不少方面都像是增强的 Map,这是由于它们的大多数 API 和行为都是共有的。
使用 new 关键字和 Set 构造函数能够建立一个空集合:
const m = new Set();
// 使用数组初始化集合 const s1 = new Set(["val1", "val2", "val3"]); alert(s1.size); // 3 // 使用自定义迭代器初始化集合 const s2 = new Set({ [Symbol.iterator]: function*() { yield "val1"; yield "val2"; yield "val3"; } }); alert(s2.size); // 3
初始化以后,可使用 add()增长值,使用 has()查询,经过 size 取得元素数量,以及使用 delete()和 clear()删除元素:
const s = new Set(); alert(s.has("Matt")); // false alert(s.size); // 0 s.add("Matt") .add("Frisbie"); // add函数返回的依然是set对象,因此可使用链式调用 alert(s.has("Matt")); // true alert(s.size); // 2 s.delete("Matt"); alert(s.has("Matt")); // false alert(s.has("Frisbie")); // true alert(s.size); // 1 s.clear(); // 销毁集合实例中的全部值 alert(s.has("Matt")); // false alert(s.has("Frisbie")); // false alert(s.size); // 0
十5、ES2015 Map数据结构
做为 ECMAScript 6 的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性均可以经过 Object 类型实现,但两者之间仍是存在一些细微的差别。具体实践中使用哪个,仍是值得细细甄别。
使用 new 关键字和 Map 构造函数能够建立一个空映射:
const m = new Map();
若是想在建立的同时初始化实例,能够给 Map 构造函数传入一个可迭代对象,须要包含键/值对数组。可迭代对象中的每一个键/值对都会按照迭代顺序插入到新映射实例中:
// 使用嵌套数组初始化映射 const m1 = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]); alert(m1.size); // 3 // 映射期待的键/值对,不管是否提供 const m3 = new Map([[]]); alert(m3.has(undefined)); // true alert(m3.get(undefined)); // undefined
const m = new Map() const tom = { name: 'tom' } m.set(tom, 90) console.log(m) console.log(m.get(tom)) // 输出 // Map { { name: 'tom' } => 90 } // 90 // 90 { name: 'tom' }
一样,map也有has()、delete()、clear()方法。