做者 | 司徒正美 责编 | 郭芮 出品 | CSDN(ID:CSDNnews) JavaScript能发展到如今的程度已经经历很多的坎坷,早产带来的某些缺陷是永久性的,所以浏览器才有禁用JavaScript的选项。甚至在jQuery时代有人问出这样的问题,jQuery与JavaScript哪一个快?在Babel.js出来以前,发明一门全新的语言代码代替JavaScript的呼声一直不绝于耳,前有VBScript,Coffee, 后有Dartjs, WebAssembly。要不是它是全部浏览器都内置的脚本语言, 可能就命绝于此。浏览器就是它的那个有钱的丈母娘。此外源源不断的类库框架,则是它的武器库,从底层革新了它本身。为何这么说呢? JavaScript没有其余语言那样庞大的SDK,针对某一个领域自带的方法是不多,好比说数组方法,字符串方法,都不超过20个,是Prototype.js给它加上的。JavaScript要实现页面动效,离不开DOM与BOM,但浏览器互相竞争,致使API不一致,是jQuery搞定了,还带来了链式调用与IIFE这些新的编程技巧。在它缺少大规模编程模式的时候,其余语言的外来户又给它带来了MVC与MVVM……这里面许多东西,长此以往都变成语言内置的特性,好比Prototype.js带来的原型方法,jQuery带来的选择器方法,实现MVVM不可缺乏的对象属性内省机制(getter, setter, Reflect, Proxy), 大规模编程须要的class, modules。 本文将如下几个方面介绍这些新特性,正是它们武装了JavaScript,让它变成一个正统的,魔幻的语言。 原型方法的极大丰富;javascript
类与模块的标准化;html
异步机制的嬗变;前端
块级做用域的补完;vue
基础类型的增长;java
反射机制的完善;node
更顺手的语法糖。jquery
原型方法的极大丰富 原型方法自Prototype.js出来后,就不断被招安成官方API。基本上在字符串与数组这两大类别扩充,它们在平常业务中不断被使用,所以不断变重复造轮子,所以亟待官方化。webpack
JavaScript的版本说明:git
这些原型方法很是有用,以至于在面试中常常被问到,若是去除字符串两边的空白,如何扁平化一个数组?es6
类与模块的标准化 在没有类的时代,每一个流行框架都会带一个建立类的方法,可见你们都不太认同原型这种复用机制。 下面是原型与类的写法比较:
function Person(name) { this.name = name; } //定义一个方法而且赋值给构造函数的原型 Person.prototype.sayName = function () { return this.name; };
var p = new Person('ruby'); console.log(p.sayName()) // ruby
class Person { constructor(name){ this.name = name } sayName() { return this.name; } } var p = new Person('ruby'); console.log(p.sayName()) // ruby 咱们能够看到es6的定义是很是简单的,而且不一样于对象键值定义方式,它是使用对象简写来描述方法。若是是标准的对象描述法,应该是这样:
//下面这种写法并不合法 class Person { constructor: function(name){ this.name = name } sayName: function() { return this.name; } } 若是咱们想继承一个父类,也很简单:
class Person extends Animal { constructor: function(name){ super(); this.name = name } sayName: function() { return this.name; } } 此外,它后面还补充了三次相关的语法,分别是属性初始化语法,静态属性与方法语法,私有属性语法。目前私有属性语法争议很是大,但仍是被标准化。虽然像typescript的private、public、protected更符合从后端转行过来的人的口味,不过在babel无所不能的今天,咱们彻底可使用本身喜欢的写法。 与类一块儿出现的还有模块,这是一种比类更大的复用单元,以文件为载体,能够实现按需加载。固然它最主要的做用是减小全局污染。jQuery时代,经过IIFE减小了这症状,可是JS文件没有统一的编写规范,意味着想把它们打包一个是很是困难的,只能像下面那样平铺着。这些文件的依赖关系,只有最初的人知道,要了几轮开发后,就是定时炸弹。此外,不要忘记,
因而后jQuery时代,国内流行三种模块机制,以seajs主体的CMD,以requirejs为主体的AMD,及nodejs自带的Commonjs。固然,后来还有一种三合一方案UMD(AMD, Commonjs与es6 modules)。 requirejs的定义与使用:
define(['jquery'], function($){ //some code var mod = require("./relative/name"); return { //some code } //返回值能够是对象、函数等 })
require(['cores/cores1', 'cores/cores2', 'utils/utils1', 'utils/utils2'], function(cores1, cores2, utils1, utils2){ //some code }) requirejs是世界第一款通用的模块加载器,尤为自创了shim机制,让许多不模范的JS文件也能够归入其加载系统。
define(function(require){ var ("#container").html("hello,seajs"); var service = require("./service") var s = new service(); s.hello(); }); //另外一个独立的文件service.js define(function(require,exports,module){ function Service(){ console.log("this is service module"); } Service.prototype.hello = function(){ console.log("this is hello service"); return this; } module.exports = Service; }); Seajs是阿里大牛玉伯加的加载器,借鉴了Requiejs的许多功能,据说其性能与严谨性超过前者。当前为了正确分析出define回调里面的require语句,还发起了一个 100 美刀赏金活动,让国内高手一展身手。 github.com/seajs/seajs…
image_1doan2vfl17ld1nin1hbm182c9b9p.png-72.9kB 相对而言,nodejs模块系统就简单多了,它没有专门用于包裹用户代码的define方法,它不须要显式声明依赖。
//world.js exports.world = function() { console.log('Hello World'); } //main.js let world = require('./world.js') world(); function Hello() { var name; this.setName = function(thyName) { name = thyName; }; this.sayHello = function() { console.log('Hello ' + name); }; }; module.exports = Hello; 而官方钦点的es6 modules与nodejs模块系统极其类似,只是将其方法与对象变成关键字。
//test.js或test.mjs import * as test from './test'; //aaa.js或aaa.mjs import {aaa} from "./aaa" const arr = [1, 2, 3, 4]; const obj = { a: 0, b: function() {} } export const foo = () => { const a = 0; const b = 20; return a + b; } export default { num, arr, obj, foo } 那怎么使用呢?根据规范,浏览器须要在link标签与script标签添加新的属性或属性值来支持这新特性。(详见:www.jianshu.com/p/f7db50cf9…
但惋惜的是,浏览器对模块系统的支持是很是滞后,而且即使最新的浏览器支持了,咱们仍是免不了要兼容旧的浏览器。对此,咱们只能奠出webpack这利器,它是前端工程化的集大成者,能够将咱们的代码经过各类loader/plugin打包成主流浏览器都认识的JavaScript语法,并以最原始的方式挂载进去。异步机制的嬗变 在JavaScript没有大规模应用前,用到异步的地方只有ajax请求与动画,在请求结束与动画结束时要作什么事,使用的办法是经典的回调。 回调 因为javascript是单线程的,咱们的方法是同步的,像下面这样,一个个执行:
A(); B(); C(); 而异步则是不可预测其触发时机:
A(); // 在如今发送请求 ajax({ url: url, data: {}, success:function(res){ // 在将来某个时刻执行 B(res) } }) C(); //执行顺序:A -> C -> B 回调函数是主函数的后继方法,基本上能保证,主函数执行后,它能在以后某个时刻被执行一次。但随着功能的细分,在微信小程序或快应用中,它们拆分红三个,即一个方法跟着三个回调。
// doc.quickapp.cn/features/sy… import share from '@system.share' share.share({ type: 'text/html', data: 'bold', success: function(){}, fail: function(){}, complete: function(){} }) 在nodejs中,内置的异步方法都是使用一种叫Error-first回调模式。
fs.readFile('/foo.txt', function(err, data) { // TODO: Error Handling Still Needed! console.log(data); }); 在后端,因为存在IO操做,异步操做很是多,异步套异步很容易形成回调地狱。因而出现了另外一种模式,事件中心,EventBus或EventEmiiter。
var EventEmitter = require('events').EventEmitter; var ee = new EventEmitter(); ee.on('some_events', function(foo, bar) { console.log("第1个监听事件,参数foo=" + foo + ",bar="+bar ); }); console.log('第一轮'); ee.emit('some_events', 'Wilson', 'Zhong'); console.log('第二轮'); ee.emit('some_events', 'Wilson', 'Z'); 事件能够一次绑定,屡次触发,而且能够将原来内部的回调拖出来,有效地避免了回调地狱。但事件中心,对于同一种行为,老是解发一种回调,不能像小程序的回调那么清晰。因而jQuery引进了Promise。 Promise Promise最初叫Deffered,从Python的Twisted框架中引进过来。它经过异步方式完成用类的构建,又经过链式调用解决了回调地狱问题。
var p = new Promise(function(resolve, reject){ console.log("========") setTimeout(function(){ resolve(1) },300) setTimeout(function(){ //reject与resolve只能二选一 reject(1) },400) }); console.log("这个先执行") p.then(function (result) { console.log('成功:' + result); }) .catch(function (reason) { console.log('失败:' + reason); }).finally(function(){ console.log("总会执行") }) 为何这么说呢?看上面的示例,new Promise(executor)里的executor方法,它会待到then, catch, finally等方法添加完,才会执行,它是异步的。而then, catch, finally则又刚好对应success, fail, complete这三种回调,咱们能够为Promise以链式方式添加多个then方法。 若是你不想写catch,新锐的浏览器还提供了一个新事件作统一处理:
window.addEventListener('unhandledrejection', function(event) { // the event object has two special properties: alert(event.promise); // [object Promise] - 产生错误的 promise alert(event.reason); // Error: Whoops! - 未处理的错误对象 });
new Promise(function() { throw new Error("Whoops!"); }); // 没有 catch 处理错误 nodejs也有相同的事件:
process.on('unhandledRejection', (reason, promise) => { console.log('未处理的拒绝:', promise, '缘由:', reason); // 记录日志、抛出错误、或其余逻辑。 }); 除此以外,esma2020年还为Promise添加了三个静态方法:Promise.all()和Promise.race(),Promise.allSettled() 。 其实chrome 60已经均可以用了。 Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内全部的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);若是参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败缘由的是第一个失败 promise 的结果。
var promise1 = Promise.resolve(3); var promise2 = 42; var promise3 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, 'foo'); });
Promise.all([promise1, promise2, promise3]).then(function(values) { console.log(values); }); // expected output: Array [3, 42, "foo"] 这个方法相似于jQuery.when,专门用于处理并发事务。 Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。此方法用于竞态的状况。 Promise.allSettled(iterable)方法返回一个promise,该promise在全部给定的promise已被解析或被拒绝后解析,而且每一个对象都描述每一个promise的结果。它相似于Promise.all,但不会由于一个reject就会执行后继回调,必须全部promise都被执行才会。 Promise不并比EventBus, 回调等优异,可是它给前端API提供了一个标杠,之后处理异步就是返回一个Promise。为后来async/await作了铺垫。 生成器 生成器generator, 不是为解决异步问题而诞生的,只是刚好它的某个特性能够解耦异步的复杂性,加之koa的暴红,人们发现原来generator还能够这样用,因而就火了。 为了理解生成器的含义,咱们须要先了解迭代器,迭代器中的迭代就是循环的意思。好比es5中的forEach, map, filter就是迭代器。
let numbers = [1, 2, 3]; for (let i = 0; i < numbers.length; i++) { console.log(numbers[i]); } //它比上面更精简 numbers.forEach(function(el){ console.log(el); }) 但forEach会一会儿把全部元素都遍历出来,而咱们喜欢一个个处理呢?那咱们就要手写一个迭代器。
function makeIterator(array){ var nextIndex = 0; return { next: function(){ return nextIndex < array.length ? {value: array[nextIndex++], done: false} : {done: true}; } }; }
var it = makeIterator([1,2,3]) console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: false} console.log(it.next()); // {value: 3, done: false} console.log(it.next()); // {done: true} 而生成器则将建立迭代器经常使用的模式官方化,就像建立类同样,可是它写法有点怪,不像类那样专门弄一个关键字,也没有像Promise那样弄一个类。
//理想中是这样的 Iterator{ exector(){ yield 1; yield 2; yield 3; } } //现实是这样的 function* Iterator() { yield 1; yield 2; yield 3; } 其实最好是像Promise那样,弄一个类,那么咱们还能够用现成的语法来模拟,但生成器,如今一个新关键字yield,你能够将它当一个return语句。生成器执行后,会产生一个对象,它有一个next方法,next方法执行多少次,就轮到第几个yield的值返回。
function* Iterator() { yield 1; yield 2; yield 3; } let it = Iterator(); console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: false} console.log(it.next()); // {value: 3, done: false} console.log(it.next()); // {value: undefined, done: true} 因为写法比较离经背道,所以一般见于类库框架,业务中不多有人使用。它涉及许多细节,好比说yield与return的混用。
function* generator() { yield 1; return 2; //这个被转换成 yield 2, 并当即设置成done: true yield 3; //这个被忽略 }
let it = generator(); console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: true} console.log(it.next()); // {value: undefined, done: true}
image_1doda17jkj7kl4u1qru1era2m316.png-322.9kB 但说了这么多,这与异步有什么关系呢?咱们之因此须要回调,事件,Promise这些,实际上是但愿能实现以同步代码的方式组件异步逻辑。yield至关一个断点,能中断程序往下执行。因而异步的逻辑就能够这样写:
function* generator() { yield setTimeout(function(){ console.log("111"), 200}) yield setTimeout(function(){ console.log("222"), 100}) } let it = generator(); console.log(it.next()); // 1 视浏览器有所差别 console.log(it.next()); // 2 视浏览器有所差别 若是没有yield,确定是先打出222,再打出111。 好了,咱们搞定异步代码以同步代码的顺序输出后,就处理手动执行next方法的问题。这个也简单,写一个方法,用程序执行它们。
function timeout(data, time){ return new Promise(function(resolve){ setTimeout(function(){ console.log(data, new Date - 0) resolve(data) },time) }) } function generator(){ let p1 = yield timeout(1, 2000) console.log(p1) let p2 = yield timeout(2, 3000) console.log(p2) let p3 = yield timeout(3, 2000) console.log(p3) return 2; } // 按顺序输出 1 2 3 / 传入要执行的gen / / 其实循环遍历全部的yeild (函数的递归) 根绝next返回值中的done判断是否执行到最后一个, 若是是最后一个则跳出去*/ function run(fn) { var gen = fn(); function next(data) { // 执行gen.next 初始data为undefined var result = gen.next(data) // 若是result.done 为true if(result.done) { return result.value }else{ // result.value 为promise result.value.then(val=>{ next(val) }) } } // 调用上一个next方法 next(); } run(generator) koa早些年的版本依赖的co库,就是基于上述原理摆平异步问题。有兴趣的同窗能够下来看看。 async/await 上节章的生成器已经完美地解决异步的逻辑以同步的代码编写的问题了,什么异常,能够直接try catch,成功则直接往下走,老是执行能够加finally语句,美中不足是须要对yield后的方法作些改造,改为Promise(这个也有库,在nodejs直接内置了util.promisefy)。而后须要一个run方法,代替手动next。因而处于语言供应链上流的大佬们想,能不能直接将这两步内置呢?而后包装一个已经被人接受的语法提供给没有见过世面的前端工程师呢?他们搜刮了一遍,还真有这东西。那就是C#有async/await。
//C# 代码 public static async Task AddAsync(int n, int m) { int val = await Task.Run(() => Add(n, m)); return val; } 这种没有学习成本的语法很快迁移到JS中,async关键字,至关于生成器函数与咱们自造的执行函数,await关键字至关于yield,但它只有在它跟着的是Promise才会中断流程执行。async函数最后会返回一个Promise,能够供外面的await关键字使用。
//javascript 代码 async function addTask() { await new Promise(function(resolve){ setTimeout(function(){ console.log("111"); resolve(), 200}) }) console.log('222') await new Promise(function(resolve){ setTimeout(function(){ console.log("333"); resolve(), 200}) }) console.log('444') } var p = addTask() console.log(p)
image_1dodd79nc1imnnm91q1b1p7qhdp1j.png-6.1kB 在循环中使用async/await:
const array = ["a","b", "c"] function getNum(num){ return new Promise(function(resolve){ setTimeout(function(){ resolve(num) }, 300) }) } async function asyncLoop() { console.log("start") for(let i = 0; i < array.length; i++){ const num = await getNum(array[i]); console.log(num, new Date-0) } console.log("end") } asyncLoop() async函数里面的错误也能够用try catch包住,也可使用上面提到的unhandledrejection方法。
async function addTask() { try{ await ... console.log('222') }catch(e){ console.log(e) } } 此外,es2018还添加了异步迭代器与异步生成器函数,让咱们处理各类异步场景更加驾轻就熟:
//异步迭代器 const ruby = { [Symbol.asyncIterator]: () => { const items = [r
, u
, b
, y
, l
, o
,u
, v
, r
, e
]; return { next: () => Promise.resolve({ done: items.length === 0, value: items.shift() }) } } } for await (const item of ruby) { console.log(item) } //异步生成器函数,async函数与生成器函数的混合体 async function* readLines(path) { let file = await fileOpen(path);
try { while (!file.EOF) { yield await file.readLine(); } } finally { await file.close(); } }
块级做用域的补完
提及做用域,你们通常认为JavaScript只有全局做用域与函数做用域,可是es3时代,它仍是能经过catch语句与with语句创造块级做用域的。
try{ var name = 'global' //全局做用域 }catch(e){ var b = "xxx" console.log(b)//xxx } console.log(b) var obj = { name: "block" } with(obj) { console.log(name);//Block块上的name block } console.log(name)//global 可是catch语句执行后,仍是会污染外面的做用域,而且catch是很耗性能的。而with更不用说了,会引发歧义,被es5严格模式禁止了。 话又说回来,之因此须要块状做用域,是用来解决es3的两个很差的设计,一个是变量提高,一个重复定义,它们都不利于团队协做与大规模生产。
var x = 1; function rain(){ alert( x ); //弹出 'undefined',而不是1 var x = 'rain-man'; alert( x ); //弹出 'rain-man' } rain(); 所以到es6中,新添了let和const关键字来实现块级做用域。这两个关键字相比var,有以下特色: 做用域是局部,做用范围是括起它的两个花括号间,即for(){}, while(){}, if(){}与单纯的{}。 它也不会提高到做用域顶部,它顶部到定义的那一行变称之为“暂时性死区”,这时使用它会报错。 变量一旦变let, const声明,就再不能重复定义,不然也报错。这种严格的错误提示对咱们调试是很是有帮助的。
let a = "hey I am outside"; if(true){ //此处存在暂时性死区 console.log(a);//Uncaught ReferenceError: a is not defined let a = "hey I am inside"; }
//let与const不存在变量提高 console.log(a); // Uncaught ReferenceError: a is not defined console.log(b); // Uncaught ReferenceError: b is not defined let a = 1; //Uncaught SyntaxError: Identifier 'a' has already been declared const b = 2; //不存在变量提高,所以块级做用域外层没法访问 if(true){ var bar = "bar"; let baz = "baz"; const qux = "qux"; } console.log(bar);//bar console.log(baz);//baz is not defined console.log(qux);//qux is not defined const声明则比let声明多了一个功能,就让目标变量的值不能再次改变,即其余语言的常量。
基础类型的增长 在javascript, 咱们经过typeof与Object.prototype.toString.call能够区分出对象的类型,过去总有7种类型:undefined, null, string, number, boolean, function, object。如今又多出两个类型,一个是es6引进的Symbol,另外一个是es2019的BigInt。
console.log(typeof 9007199254740991n); // "bigint" console.log(typeof Symbol("aaa")); // "symbol" Symbol拥有三个特性,建立的值是独一无二的,附加在对象是不可遍历的,不支持隐式转换。此外Symbol上面还有其余静态方法,用来为对象扩展更多功能。 咱们先看它如何表示独一无二的属性值。若是没有Symbol,咱们寻常表示常量的方法是不可靠的。
const COLOR_GREEN = 1 const COLOR_RED = 2 const LALALA = 1;
function isSafe(args) { if (args === COLOR_RED) return false if (args === COLOR_GREEN) return true throw new Error(非法的传参: ${args}
) } console.log(isSafe(COLOR_GREEN)) //true console.log(isSafe(COLOR_RED)) //false console.log(isSafe(LALALA)) //true 若是是Symbol,则符合咱们的预期:
const COLOR_GREEN = Symbol("1")//传参能够是字符串,数字,布尔或不填 const COLOR_RED = Symbol("2") const LALALA = Symbol("1")
function isSafe(args) { if (args === COLOR_RED) return false if (args === COLOR_GREEN) return true throw new Error(非法的传参: ${args}
) } console.log(isSafe(COLOR_GREEN)) //true console.log(isSafe(COLOR_RED)) //false console.log(COLOR_GREEN == LALALA) //false console.log(isSafe(LALALA)) //throw error 注意,Symbol不是一个构造器,不能new。new Symbel("222")会抛错。 第二点,过往的对象属性都是字符串类型,若是咱们没有用Object.defineProperty作处理,它们都能直接用for in遍历出来。而Symbol属性不同,遍历不出来,所以适用作对象的私有属性,由于你只有知道它的名字,才能访问到它。
var a = { b: 11, c: 22 } var d = Symbol(); a[d] = 33 for(var i in a){ console.log(i, a[i]) //只有b,c } 第三点,以往的数据类型均可以与字符串相加,变成一个字符串,或者减去一个数字,隐式转换为数字;而Symbol则直接抛错。
ar d = Symbol("11") console.log(d - 1) 咱们再来看它的静态方法: Symbol.for 这相似一个Symbol(), 可是它不表示独一无二的值,若是用Symbor.for建立了一个symbol, 下次再用相同的参数来访问,是返回相同的symbol。
Symbol.for("foo"); // 建立一个 symbol 并放入 symbol 注册表中,键为 "foo" Symbol.for("foo"); // 从 symbol 注册表中读取键为"foo"的 symbol
Symbol.for("bar") === Symbol.for("bar"); // true,证实了上面说的 Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbol
var sym = Symbol.for("mario"); sym.toString(); 上面例子是从火狐官方文档拿出来的,提到注册表这样的东西,换言之,咱们全部由Symbol.for建立的symbol都由一个内部对象所管理。 Symbol.keyFor Symbol.keyFor()方法返回一个已注册的 symbol 类型值的key。key就是咱们的传参,也等于同于symbol的description属性。
let s1 = Symbol.for("111"); console.log( Symbol.keyFor(s1) ) // "111" console.log(s1.description) // "111"
let s2 = Symbol("222"); console.log( Symbol.keyFor(s2)) // undefined console.log(s2.description) // "222"
let s3 = Symbol.for(111); console.log( Symbol.keyFor(s3) ) // "111" console.log(s3.description) // "111" 须要注意的是,Symbol.for()为 Symbol 值登记的名字,是全局环境的,能够在不一样的 iframe 或 service worker 中取到同一个值。
iframe = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); iframe.contentWindow.Symbol.for('111') === Symbol.for('111')// true
Symbol.iterator 在es6中添加了for of循环,相对于for in循环,它是直接遍历出值。究其缘由,是由于数组原型上添加Symbol.iterator,它就是一个内置的迭代器,而for of就是执行函数的语法。像数组,字符串,arguments, NodeList, TypeArray, Set, Map, WeakSet, WeatMap的原型都加上Symbol.iterator,所以均可以用for of循环。
console.log(Symbol.iterator in new String('sss')) // 将简单类型包装成对象才能使用in console.log(Symbol.iterator in [1,2,3] ) console.log(Symbol.iterator in new Set(['a','b','c','a'])) for(var i of "123"){ console.log(i) //1,2 3 } 但咱们对普通对象进行for of循环则遇到异常,须要咱们自行添加。
Object.prototype[Symbol.iterator] = function() { var keys = Object.keys(this); var index = 0; return { next: () => { var obj = { value: this[keys[index]], done: index+1 > keys.length }; index++; return obj; } }; }; var a = { name:'ruby', age:13, home:"广东" } for (var val of a) { console.log(val); } Symbol.asyncIterator Symbol.asyncIterator与for await of循环一块儿使用,见上面异步一节。 Symbol.replace、search、split 这几个静态属性都与正则有关,咱们会发现这个方法名在字符串也有相同的脸孔,它们就是改变这些方法的行为,让它们能接收一个对象,这些对象有相应的symbol保护方法。具体见下面例子:
class Search1 { constructor(value) { this.value = value; } Symbol.search { return string.indexOf(this.value); } }
console.log('foobar'.search(new Search1('bar'))); class Replace1 { constructor(value) { this.value = value; } Symbol.replace { return s/${string}/${this.value}/g
; } }
console.log('foo'.replace(new Replace1('bar'))); class Split1 { constructor(value) { this.value = value; } Symbol.split { var index = string.indexOf(this.value); return this.value + string.substr(0, index) + "/" + string.substr(index + this.value.length); } }
console.log('foobar'.split(new Split1('foo'))); Symbol.toStringTag 能够决定自定义类的 Object.prototype.toString.call的结果:
class ValidatorClass { get Symbol.toStringTag { return 'Validator'; } }
console.log(Object.prototype.toString.call(new ValidatorClass())); // expected output: "[object Validator]" 此外,还有许多静态属性, 方便咱们对语言的底层作更精致的制定,这里就不一一罗列了。 咱们再看BigInt, 它就没有这么复杂。早期JavaScript的整数范围是2的53次方减一的正负数,若是超过这范围,数值就不许确了。
console.log(1234567890123456789 * 123) //这显然不对 所以咱们很是须要这样的数据类型,在它没有出来前只能使用字符串来模拟。而后chrome67中,已经内置这种类型了。想使用它,可能直接在数字后加一个n,或者使用BigInt建立它。
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991); // ↪ 9007199254740991n
const hugeString = BigInt("9007199254740991"); // ↪ 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff"); // ↪ 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111"); console.log(typeof hugeBin) //bigint
反射机制的完善
反射机制指的是程序在运行时可以获取自身的信息。例如一个对象可以在运行时知道本身哪些属性被执行了什么操做。 最早映入咱们眼帘的是IE8带来的get, set关键字。这就是其余语言的setter, getter。看似是一个属性,实际上是两个方法。
var inner = 0; var obj = { set a(val){ console.log("set a ") inner = val }, get a(){ console.log("get a ") return inner +2 } } console.log(obj) obj.a = 111 console.log(obj.a) // 113
image_1dojfhdi1vqbdqg1hr4mkt52h9.png-11.9kB
但在babel.js尚未诞生的年代,新语法是很难生存的,所以IE8又搞了两个相似的API,用来定义setter, getter:Object.defineProperty与Object.defineProperties。后者是前者的强化版。
var inner = 0; var obj = {} Object.defineProperty(obj, 'a', { set:function(val){ console.log("set a ") inner = val }, get: function(){ console.log("get a ") return inner +2 } }) console.log(obj) obj.a = 111 console.log(obj.a) // 113 而标准浏览器怎么办?IE8时代,firefox一方也有相应的私有实现:defineGetter, defineSetter,它们是挂在对象的原型链上。
var inner = 0; var obj = {} obj.defineSetter("a", function(val){ console.log("set a ") inner = val }) obj.defineGetter("a", function(){ console.log("get a ") return inner + 4 })
console.log(obj) obj.a = 111 console.log(obj.a) // 115 在三大框架没有崛起以前,是MVVM的狂欢时代,avalon等框架就是使用这些方法实现了MVVM中的VM。 setter与getter是IE停滞十多年瀦中添加的一个重要特性,让JavaScript变得现代化,也更加魔幻。 但它们只能监听对象属性的赋值取值,若是一个对象开始没有定义,后来添加就监听不到;咱们删除一个对象属性也监听不到;咱们对数组push进一个元素也监听不到,对某个类进行实例化也监听不到……总之,局b限仍是很大的。因而chrome某个版本添加了Object.observe(),支持异步监听对象的各类举动(如"add", "update", "delete", "reconfigure", "setPrototype", "preventExtensions"),可是其余浏览器不支持,因而esma委员会又合计搞了另外一个逆天的东西Proxy。 Proxy 这个是es6大名鼎鼎的魔术代理对象,与Object.defineProperty同样,没法以旧有方法来模拟它。 下面是它的用法,其拦截器所表明的操做:
let p = new Proxy({}, {//拦截对象,上面有以下拦截器 get: function(target, name){ // obj.aaa }, set: function(target, name, value){ // obj.aaa = bbb }, construct: function(target, args) { //new }, apply: function(target, thisArg, args) { //执行某个方法 }, defineProperty: function (target, name, descriptor) { // Object.defineProperty() }, deleteProperty: function (target, name) { //delete }, has: function (target, name) { // in }, ownKeys: function (target, name) { // Object.getOwnPropertyNames() // Object.getOwnPropertySymbols() // Object.keys() Reflect.ownKeys() }, isExtensible: function(target) { // Object.isExtensible()。 }, preventExtensions: function(target) { // Object.preventExtensions() }, getOwnPropertyDescriptor: function(target, prop) { // Object.getOwnPropertyDescriptor() }, getPrototypeOf: function(target){ // Object.getPrototypeOf(), // Reflect.getPrototypeOf(), // proto // Object.prototype.isPrototypeOf()与instanceof }, setPrototypeOf: function(target, prototype) { // Object.setPrototypeOf(). } }); 这个对象在vue3, mobx中被大量使用。 Reflect Reflect与Proxy一同推出,Reflect上的方法与Proxy的拦截器同名,用于一些Object.xxx操做与in, new , delete等关键字的操做(这时只是将它们变成函数方式)。换言之,Proxy是接活的,Reflect是干活的,火狐官网的示例也体现这一点。
var p = new Proxy({ a: 11 }, { deleteProperty: function (target, name) { console.log(arguments) return Reflect.deleteProperty(target, name) } }) delete p.a 它们与Object.xxx最大的区别是,它们都有返回结果, 而且传参错误不会报错(如Object.defineProperty)。可能官方认为将这些元操做方法放到Object上有点不妥,因而推出了Reflect。 Reflect总共有13个静态方法:
Reflect.apply(target, thisArg, args) Reflect.construct(target, args) Reflect.get(target, name, receiver) Reflect.set(target, name, value, receiver) Reflect.defineProperty(target, name, desc) Reflect.deleteProperty(target, name) Reflect.has(target, name) Reflect.ownKeys(target) Reflect.isExtensible(target) Reflect.preventExtensions(target) Reflect.getOwnPropertyDescriptor(target, name) Reflect.getPrototypeOf(target) Reflect.setPrototypeOf(target, prototype)
更顺手的语法糖
除了添加这些方法外,JavaScript底层的parser也大动手术,让它支持更多语法糖。语法糖均可以写成对应的函数,但不方便。总的来讲,语法糖是想让你们的代码更加精简。 新近添加以下语法糖: 对象简写,参看类的组织形式
扩展运算符(…),用于对象的浅拷贝
箭头函数,省略function关键字,与数学公式走近,能绑定this与略去return
for of(遍历可迭代对象的全部值, for in是遍历对象的键或索引)
数字格式化, 如1_222_333
字符串模板化与自然多行支持,如hello ${world}
幂运算符, **
可选链,let x = foo?.bar.baz();
空值合并运算符, let x = foo ?? bar();
函数的默认参数
总结
ECMAScript正在快速发展,常常会有新特性被引入,有兴趣能够查询babel的语法插件(www.babeljs.cn/docs/plugin… 做者简介:司徒正美,拥有十年纯前端经验,著有《JavaScript框架设计》一书,去哪儿网公共技术部前端架构师。爱好开源,拥有mass、Avalon、nanachi等前端框架。目前在主导公司的小程序、快应用的研发项目。 【END】 ———————————————— 版权声明:本文为CSDN博主「CSDN资讯」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处连接及本声明。 原文连接:blog.csdn.net/csdnnews/ar…