Web瞎捣鼓——JavaScript终章

廉价感情. 提交于 2020-10-27 03:26:09

Web的大杂烩马上就要告一段落,我也想弄点好东西出来,不然就成了只会复制没有思考的废物了。“Web瞎捣鼓”系列还会继续,只是不搞这种几千字的不知所云的东西,尽量弄点硬核的东西出来。这不是终章,而是新的起点。别揍我,这篇还会继续的有点虚头巴脑!

新特性

1、先上场和变量有关,前一篇已经写了let和const命令,就不再赘言。这里要提到的是数组和对象的解构赋值,就是按照一定的模式从数组和对象中提取值对变量进行赋值。它可以是不完全解构,只要模式匹配就能正确解构,可以使用reset参数来进行模式匹配。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

解构赋值允许默认值,解构目标匹配位置必须严格等于undefined,默认值才会有效。

let [x = 1] = [undefined];
x // 1

let [y = 1] = [null];
y // 1


// 对象的结构赋值,m和n是匹配模式最终赋值给a和b
let {m: a = 3} = {m: undefined};
a // 3

let {n: b = 3} = {n: null};
b // null

2、新增的数据类型Symbol,表示独一无二的值。

let s = Symbol();

typeof s
// "symbol"

3、新增数据结构Set和Map

  • Set类似于数组,成员的值都是唯一的,没有重复的值;对一维数组参数的值转换为自身成员。
  • Map类似于对象,键值对集合(Hash结构);对二维数组参数的值转换为自身成员。

Map的键若是一个简单数据类型,严格相等则视为一个键;若是复合型数据结构内存地址一样可视为同一个键;undefined和null视为同一个键。

与它们相似的还有WeakSet和WeakMap:

  • WeakSet 的成员只能是对象,而不能是其他类型的值;对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。
  • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名;键名所指向的对象,不计入垃圾回收机制。

4、对字符串、正则、数值、函数、数组、对象进行扩展

  • 字符串扩展:加强了对Unicode的支持;增加模版字符串`${}`省去用+连接;新增标签模版。另外还新增了一些属性方法和实例方法,如includes()、startsWith()等。
  • 正则扩展:增加u修饰符、y修饰符、s修饰符、unicode属性、sticky属性、flags属性、具名匹配、正则索引等。
  • 数值扩展:为二进制和八进制提供新的写法;扩展Number、Math对象;新增指数运算符、BigInt数据类型。
  • 函数扩展:函数参数增加缺省参数和reset参数;新增箭头函数;catch参数允许省略。
  • 数组扩展:新增扩展运算符;新增find()、findIndex()、includes()、entries(),keys()、values()等方法。
  • 对象扩展:简洁表示法允许直接写入变量和函数;新增super关键字指向原型对象;扩展运算符可用于对象;新增链判断符?.判断对象是否存在、空运算符??进行判空设置默认值。另外还新增了一些属性方法,如assign()、fromEntries()、entries()、keys()、values()等。

6、Iterator和for ... of循环

Iterator的遍历过程:创建一个指针对象指向数据结构的起始位置,调用next()方法,指向成员,返回具有velue和done属性的对象,直到done为true表示结束。除了next()方法,还有报错调用的return()、配合Generator函数的throw()。

Iterator 的作用有三个:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

只要数据结构部署了Iterator就被认为可遍历,默认Itrator部署在Symbol.iterator属性上。具有Iterator的数据结构有Array、Map、Set、String、TypedArray、arguments、NodeList。除了这些数据结构,对象、数组、Map、Set调用对应的entries()、keys()、values()后会返回遍历器对象。

for...of与for...in的区别:

  • for...in主要是针对对象设计,会遍历到手动添加的其他键值,甚至会遍历到原型链上的键值。遍历数组时键值是索引的字符串。
  • for...of可以遍历所有具有Iterator的数据结构,对象本身不具备Iterator不能直接遍历,也就没有for...in的缺点。

for...of 用于遍历同步的Iterator,为了遍历异步Iterator,ES6增加了 for await...of。

7、异步与同步

众所周知JavaScript是单线程语言,就是一次只能执行一个任务,前一个任务执行完毕才去执行后一个任务。这个模式在设计上规避了一些问题,但也制造了新的麻烦。举例来说,我们用程序来描述做家务,当前有扫地、拖地、烧开水、泡茶。扫地、拖地不能同时执行(同步任务),然而我们期望,先烧开水,然后挂起这个任务去扫地、拖地(异步任务)。为了解决这个冲突充分利用CPU,于是出现了“事件循环”机制。

当然这不能改变JavaScript单线程运行的本质:运行时将同步任务压栈处理,异步任务放入消息队列,等执行栈任务为空再执行消息队列中的可执行的任务,如此形成事件循环。(ps:JavaScript只是在一个主线程上运行,还有其他线程进行辅助处理)

传统的异步解决方案有定时器、事件、回调等。值得一提的是0延迟:定时器延迟为0并不代表它就能马上执行,它同样会进入消息队列等待。定时器的执行取决于队列里待处理的任务数量,所以定时器并不是会守时。

在ES6中引入了新的解决方案Promise,它比传统的解决方案更强大更合理。拿回调回调来说,在逻辑复杂的情况下,通常会一层回调嵌套另一层回调形成成回调地狱,造成代码复杂不易维护。再拿事件来说,事件一旦触发,再监听是不能再次得到结果的。Promise也并不是完美的解决方案,也有缺点:Promise一旦创建就会立即执行就无法取消;有时Promise内部抛错不会传递到外部;当promise处于pending状态,无法得知目前处于哪个阶段。

const promise = new Promise(function(resolve, reject) {
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Generator函数是ES6提供的另一种异步解决方案,它与传统的函数完全不同。从形式上它是一个普通函数使用function定义,但必须在function后跟一个星号。调用Generator函数会返回一个Iterator,也就是说需要执行next方法才能执行下一个状态,下一个状态在yield表达式或者return语句或者函数结束符停下。Generator函数也只有在next方法调用后才开始执行。

function* helloGenerator() {
  yield 'hello';
  yield 'generator';
  return 'ending';
}

var hg= helloGenerator();
hg.next();  // {value:'hello',done:false}
hg.next();  // {value:'generator',done:false}
hg.next();  // {value:'ending',done:true}
hg.next();  // {value:undefined,done:true}

yield可以在Generator函数中多次使用,相当于多次return,但只能在Generator函数中使用,且必须是yield所在函数块。yield返回值是undefined,next方法可以带一个参数当作上一个yield的返回值。Generator函数中指针移动不会改变上下文,那么这个next传入的返回值可以缓存下来,不受yield间隔影响。也就是说,next()的执行不单纯是yield后边表达式的结果,要注意前后关联。

const arr = [1,2]
function* testGenerator () {
  arr.forEach(() => {
	yield 'test';  // SyntaxError
  });
}

function* testYield (x) {
  // yield不在=旁边须加()
  const y = 4 + (yield);
  yield y;
  yield y + x;
}
const tg = testGenerator(4);
const tg1 = tg.next(1);  // {value:undefined,done:false}
const tg2 = tg.next(2);  // {value:6,done:false}
const tg3 = tg.next(3);  // {value:10,done:false}

Generator函数运行时是Iterator,因此它可以使用for...of进行遍历不用next方法取到值,但是当next方法返回done为true时就会中止(即:return返回的值会被忽略)。

如果Generator函数内部调用一个Generator函数(也就是yield Iterator),内层遍历器对象不会执行。需要使用yield*,如return有返回值需要处理。另外,它也可以遍历其他具有Iterator部署的数据结构。

再来看看async函数,事实上它就是Generator函数的语法糖,星号换成了async关键字,yeild换成了await。只不过它粘了别的糖——内置执行器并将执行结果以Promise返回。async函数虽然是异步解决方案,但是await具有同步效果,await会等其后异步操作执行完成后才会接着执行后面的的操作。如果遇到错误就会中断后面的执行,所以在使用过程中需要注意错误的处理。如果不存在继发关系,最好同时触发。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

8、Reflection

Proxy用于修改某系操作的默认行为,属于一种元编程。

Reflect可以获取语言内部方法,使Object的操作更合理,与Proxy的方法一一对应完成默认行为。

9、Class定义一个类,这相当于一个语法糖,让语义更加清晰、写法更加简洁。使用static关键字定义静态方法,在新的提案中可以修饰属性。定义属性时将变量定义在类的顶层,可以不用在属性前加this。在新的提案中使用#表示,目前可以使用Symbol来定义,但是私有属性和方法过多就会显得十分笨重。还可在Class外部定义,这就与Class的设计有点尴尬。

// ES5
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

使用extends进行继承,这比ES5修改原型链清晰方便。super作为函数调用时代表父类构造函数,子类的构造函数必须一次super函数;作为对象时指向父类。

** 注意 **

使用class关键字构造的方法应特别注意this的指向:当使用static关键字时this指向构造函数;当使用用new关键字构造实例对象,this指向实例;当通过解构实例对象后的this是undefined;extends子类在super()调用父类之前不能使用this。

10、ES6新语法不止这些,还有模块体系、globalThis、ArrayBuffer等。还有一些尚是提案,比如管道运算符、数值分隔符、双冒号运算符、装饰器等。

比较

1、数组的遍历:数组的遍历方法有很多种,其中最容易想到的就是循环(while/for/for...of)。循环与其它方法相比,最突出的就是它可以被break/return打断,这里要对比的是其他方法。

在这之前要先说一个问题,之前有人问我map方法会改变原数组吗?我的回答是也许会,这得看怎么操作。当然也要看数组里是什么数据,基础数据类型肯定不会,对象就会。数组遍历的回调函数的参数是对数组每个对象的内存地址的引用,然后对参数修改就会影响原数组。所以,我认为不管哪种遍历方式都会是一样的结果,影响这个结果的还有操作手法。

这里还有一个情况,有时候我们需要一个数组副本,然后对副本进行操作。网上查深拷贝数组,最多的方法就是扩展运算符。事实上,这个方法不对,它的错和上面的差不多,扩展运算符对对象的拷贝是浅拷贝。因此,只要对数组副本操作,也会直接影响原数据。

数组遍历方法的区别主要在功能上:

  • forEach() - 遍历数组中的每一个元素
  • every() - 根据回调函数的条件,每个元素都满足返回true,否则返回false
  • some() - 根据回调函数的条件,至少有一个满足返回true,否则返回false
  • filter() - 满足回调函数的条件的元素放进新数组并返回
  • map() - 返回由回调函数返回值组成的新数组
  • reduce() - 从左到右执行回调函数,回调函数的返回值传给下一个回调,返回最后一次回调函数的值
  • reduceRight() - 从右到左执行回调函数,回调函数的返回值传给下一个回调,返回最后一次回调函数的值

2、Map和Set已经作过比较,Set和Array区别主要表现在API上,接下来主要比较Object和Array、Map和Object。

Object Vs Array:

  • Object是无序键值对,Array是有序值;
  • Object是可以伪装成Array,但需要自己设置长度,Aarry不需要;
  • Array有Iterator,Object需要手动部署。

Map Vs Object:

  • Map没有默认键值,Object有一个原型链;
  • Map键可以是任意类型,Object必须是String或Symbol;
  • Map是有序的,Object是无序的;
  • Map可以获取元素个数,Object只能手动计算;
  • Map有Iterator,Object需要手动部署。

(ps:记得好像还有好些个,一下全忘了,后面想起了再加吧)

API

JavaScript的组成部分有两部分都是API:DOM和BOM。对于浏览器来说,API实在太多了:文档对象模型(Document Object Model)、设备APIs(Device APIs)、通信APIs(Communication APIs)、数据管理 APIs(Data management APIs)、数据管理 APIs(Data management APIs)等。下面从MDN上接了个图,就不细说了:

最后

这文章越往后写越心虚的要紧,越写越觉得自己对于JavaScript了解得不够,尤其是每次看完阮一峰的书之后,感觉自己一直翻来覆去在和语法打架。还有好多没涉及到:Web存储、Web SQL、应用程序缓存、Web Workers、SSE、WebSocket、WebAssembly、HTTP……快数不过来了……

接下来还有很长的路要走,前端工程化:前端变革主导者Node;动态样式SCSS/LESS;前端三大框架VUE/React/Angular;打包工具Webpack/Grunt;单元测试Jest/Mocha;可以编译成JavaScript的TypeScript/Dart;Hybird开发框架Flutter/UniApp;小程序框架Wepy/Taro……

当然,不能再以这样的状态和思路去写文章,毫无意义。且行且看且折腾吧!

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!