ES6部分总结

JavaScript,前端 2017-12-06

var、let、const 比较

var 会有声明提升,并初始化为 undefined

console.log(a)  //  undefined
var a = 1
console.log(a)  //  1

console.log(b)  //  Uncaught ReferenceError: b is not defined
b = 3
console.log(b)  //  3
console.log(window.b)  //  3

疑问:

  • 为什么打印 b 会报错?
  • 为什么 window.bb 结果是一样的?

因为 var 声明有会一个提升。因为变量 b 没有带声明的关键字,在非严格模式下 b 就会被当作是全局变量,全局变量又会自动加到全局对象属性里,浏览器里的全局对象就是 window,所以才可以用 window.b 调用。关于 var 声明提升可以看下面代码:

var a = 'undefined'
console.log(a)  //  undefined
a = 1
console.log(a)  //  1

let 有声明提升,但是不会初始化为 undefined ,而是保存在暂存区(TDZ)里。

let a = 'global'
{
  console.log(a)  //  Uncaught ReferenceError: a is not defined
  let a = 1
}

为什么不是打印出 'global',而是报错了?如果把 let a = 1 这个语句去掉,是可以正常打印出 'global' 的,所以代码应该是这样的:

let a = 'global'
{
  let a  //  这时候,变量 a 进了暂存区,除非 `let a = 1` 执行完,它才会出暂存区,才能被调用。
  console.log(a)  //  Uncaught ReferenceError: a is not defined
  a = 1
}

let 是不允许重复声明的,是有块级作用域的。

//  不允许重复声明
let a = 1
let a = 2  //  Uncaught SyntaxError: Identifier 'a' has already been declared

//  没有块级作用域
var x = 1
{
  var x = 2
}
console.log(x)  //  2

//  有块级作用域
let x = 1
{
  let x = 2
}
console.log(x)  //  1

const 是常量声明,声明时要指定初始值,也有块级作用域。

const c = 1
{
  const c = 2
}
console.log(c)  //  输出1,而且不会报错

最佳实践

  • 常量用 const,能用 const 就不要用 let
  • 能用 let 就不要用 var

箭头函数

一种语法糖:
语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。(概念来自维基百科)

ES6 允许使用“箭头”(=>)定义函数。

let f = v => v
//  等同于
let f = function(v) {
  return v
}

let f = () => 5
//  等同于
let f = function () { return 5 }

let sum = (num1, num2) => num1 + num2
//  等同于
let sum = function(num1, num2) {
  return num1 + num2
}

//  这个涉及到与变量解构的使用,后面讲。
const full = ({ first, last }) => first + ' ' + last;

//  等同于
function full(person) {
  return person.first + ' ' + person.last;
}

部分语法:

  • 参数列表右括号要和箭头在同一行上。
  • 单行箭头函数,函数体只能有一个语句。
  • 若箭头函数返回一个对象字面量,要用括号括起来。

特性:
this 指向是固定的,不可改的,将函数内部的 this 延伸到上一层作用域。

//  ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

//  使用 Bable 转为 ES5 的代码如下:
'use strict';

function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
  //  也就是说箭头函数的 `this` 指向的是 foo() 函数体里的 `this`,而不是匿名函数的 `this`
}
function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);  //  s1: 3
setTimeout(() => console.log('s2: ', timer.s2), 3100);  //  s2: 0
setTimeout(() => console.log(s2), 3300);  //  NaN

为什么 console.log(s2) 没有报错,而是 NaN ??这是因为在第二个定时器里头,回调函数是一个匿名函数嘛,所以它的 this 指向的是全局对象(windows),所以 this.s2++; 就等同于 window.s2++; 也等同于全局变量 s2++s2 初始值应该是 undefined,所以 s2++ 就变成 NaN

模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量、调用函数等。

以前如果写 html 可能要用到很多字符串拼接。

var str =
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'

现在利用模板字符串就可以大大简化工作了。

var str =`
  There are <b> ${basket.count} </b> items in your basket,  
  <em> ${basket.onSale} </em> are on sale!
  `

变量的解构赋值

实际场景:我请求接口,然后返回的是一个对象。

//  假设返回的就是这个 data
data = {
    name: 'tao',
    age: 20,
    sex: '男'
}

//  我要拿 data 里头的属性,以前我可能这样写:
let str = `
    <span>姓名:${data.name}</span>
    <span>年龄:${data.age}</span>
    <span>性别:${data.sex}</span>
`
//  这时候,写接口的很坑,假设他给我那个 age 是个空值,没有初始值的。这时候我没做检测的话页面就崩了。
//  利用对象解构可以这样写:
let {name, age, sex} = data
let str = `
    <span>姓名:${name}</span>
    <span>年龄:${age}</span>
    <span>性别:${sex}</span>
`
//  这时候是不是简化了,还有就是没有值,页面也不会崩了,因为都给初始值为 undefined

//  还有一个场景就是,楚育把 name 用烂了,用了很多次,那如果我还用 name 的话,就冲突了。所以可以下面这样

let { name: className } = { name: '15软2', bar: "bbb" }
console.log(className) // '15软2'

... 操作符

展开运算符(用三个连续的点 ( ... ) 表示)是 ES6 中的新概念,使你能够将 字面量对象 展开为多个元素。

const fruits = ["apples", "bananas", "pears"];
const vegetables = ["corn", "potatoes", "carrots"];

const produce = [...fruits,...vegetables];

console.log(produce);  //  [ 'apples', 'bananas', 'pears', 'corn', 'potatoes', 'carrots' ]

//  在之前的话,结合数组可能要用到 concat 方法

剩余参数也用三个连续的点 ( ... ) 表示,使你能够将不定数量的元素表示为数组。

function sum(...nums) {
  let total = 0;  
  for(const num of nums) {
    total += num;
  }
  return total;
}

sum(10, 36, 7, 84, 90, 110);  //  337

//  其中 for...of 是一个新的循环形式,该循环将只循环访问对象中的值。
const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const number of numbers) {
  console.log(number);
}
//  依次打印出 0 到 9

默认函数参数

function say(message = 'hello') {
  console.log(message)
}

say()  //  hello
say('你好')  //  你好
//  这种用法我记得 PHP 也有,挺方便的。

把默认参数和解构结合起来

function quadrature([sum1 = 2, sum2 = 4]){
  console.log(sum1 * sum2)
}
quadrature([])  //  8
quadrature([4])  //  16
quadrature([, 5])  //  10
quadrature([3, 4])  // 12

//  这种写法有个不好的地方就是如果不传参数就会报错
quadrature()  //  Uncaught TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined

//  改成这样就不会了
function quadrature([sum1 = 2, sum2 = 2] = []){
  console.log(sum1 * sum2)
}
function quadrature({sum1 = 2, sum2 = 2, arrs = ['hello', '你好']} = {}){
  
  console.log(sum1 * sum2)

  for (const arr of arrs){
    console.log(arr)
  }
  
}
quadrature()  //  4  hello  你好

对象解构与数组解构的区别

使用对象解构,它是根据键值对去配对,而数组解构,它是基于位置的。个人建议用对象解构比较好。

Class 类

关键字 class constructor() static extends super

es6-class.png

通过对比,我们可以看出:
Event 类里头的构造函数 constructor() 好像就是右边的 function Event(){}

prototype.png

其实我们在 new Event() 的时候,它就是调用了 constructor() 然后它会返回一个对象,而这个对象它的 __proto__ 会指向 Event.prototpye

然后我们在调用 event.on(),因为它本身是没有这个方法的,所以会去 event.__proto__ 指向的 Event.prototpye 去找,如果 Event.prototpye 也没有,会继续根据 Event.__proto__ 指向的 Function.prototype 里头找,会根据原型链一直到找下去,直到 __proto__ 指向 null 这时会报错说不存在该方法。

继承

//  stream.js

const Writable = require('stream').Writable; 
const util = require('util');

module.exports = class MyStream extends Writable {
    constructor(matchText, options) {
        super();
        this.count = 0;
        this.matcher = new RegExp(matchText, 'ig');
    }

    _write(chunk, encoding, cb) {
        let matches = chunk.toString().match(this.matcher);
        if (matches) {
            this.count += matches.length;
        }
        if (cb) {
            cb();
        }
    }

    end() {
        this.emit('total', this.count);
    }
}

我们可以看到,只是简单的用关键字 extends,然后里面有一点要注意的是,你用子类的构造器时,要先调用 super(),不然会报错。然后还有其他用法,我自己也没怎么试,我就不细写,其实我是想早点回宿舍。。。

这只是一部分代码,整份代码可以看这个 node.js-demo

ES5 的继承可以看我之前写的 JavaScript类的继承

ES6 Module

不打算细讲,因为浏览器支持不是很好,如果我们团队以后用 Webpack 等打包工具了,那没问题,直接通过 Babel 打包成兼容 ES5 的文件,那就比较方便。但是现在我们还在用。。

在此之前市面上的模块实现的规范有这么几种,CommonJS、AMD、CMD、UMD。像 Node.js 就是用 CommonJS 那一套,RequireJS 就是 AMD,CMD 的话有 Sea.js 等等,你们可以去了解这些关于模块的规范。现在 ES6 也有了自己的模块。

好,接下来。讲我们的主角

关于模块有两个关键字:importexport

接触过编程的人,一看就大概知道这两个干嘛用的吧。import 肯定就是导入东西,就好像 Java 的导入包也是用这个关键字嘛。那 export 应该就是导出,输出的意思吧。

//  sum.js
export default (a, b) => console.log(a + b)

//  写成这样也没问题
function sum (a, b) {
  console.log(a + b)
}

export {sum as default}

//  为什么会多一个 default ,不这样的话,我导入的时候变量名要跟导出的变量对应得上。
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Module Demo</title>
</head>
<body>
  <h1> ES6 </h1>
</body>
<script type="module">
  import fn from './sum.js'
  fn(5, 6)  //  11
</script>
</html>

注意

  • 导入的时候要写文件后缀名,不像 Node.js 的 require 方法一样,js 文件可以不加后缀。
  • 要以'/''./''../'开头,反正现在还不支持直接用 sum.js
  • 会有 CORS 检查,也就是说会有跨域问题,比如你直接右键文件用浏览器打开肯定是报错的,你要放服务器里头,用本地主机去访问。
  • 记得脚本标签要加上 type="module"

可以参考这些文档

题外话

如果对于作用域,声明前置,this 指向不怎么清楚的,可以看工作室书架里的书 《你不知道的JavaScript 上卷》。然后更多关于 ES6 的新特性可以看下面参考资料的链接。

参考资料


本文由 阿涛 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论