ES6笔记(一):ES6所改良的javascript“缺陷”

块级做用域

ES5没有块级做用域,只有全局做用域和函数做用域,因为这一点,变量的做用域甚广,因此一进入函数就要立刻将它建立出来。这就形成了所谓的变量提高。javascript

ES5的“变量提高”这一特性每每一不当心就会形成一下错误:html

  1. 内层变量覆盖外层变量java

    var tmp = new Date();
     function f() {
       console.log(tmp);
       if (false) {    //执行则undefined
         var tmp = "hello world";
       }
     }
  2. 变量泄露,成为全局变量闭包

    var s = 'hello';
     for (var i = 0; i < s.length; i++) {
       console.log(s[i]);
     }
     console.log(i); // 5

往常咱们每每是使用闭包来解决这一问题的(好比自执行函数)。如今,基于这一问题,ES6增长了块级做用域,因此再也不须要自执行函数了。app

let 和 const

ES6是是向后兼容的,而保持向后兼容性意味着永不改变JS代码在Web平台上的行为,因此var建立的变量其做用域依旧将会是全局做用域和函数做用域。这样以来,即便拥有了块级做用域,也没法解决ES5的“变量提高”问题。因此,这里ES6新增了俩个新关键词:letconst模块化

  1. let函数

    “let是更完美的var”,它有着更好的做用域规则。this

  2. const
    const声明一个只读的常量。一旦声明,常量的值就不能改变,但const声明的对象能够有属性变化(对象冻结Object.freeze)prototype

    const a = [];
     a.push('Hello'); // 可执行
     a = ['Dave'];    // 报错

    也可使用Object.freeze将对象冻结设计

    const foo = Object.freeze({});
     // 常规模式时,下面一行不起做用;
     // 严格模式时,该行会报错
     foo.prop = 123;//

使用let和const:

  • 变量只在声明所在的块级做用域内有效

  • 变量声明后方可以使用(暂时性死区)

  • 不能重复定义变量

  • 声明的全局变量,不属于全局对象的属性

    var a = 1;
      window.a // 1
      let b = 1;
      window.b // undefined

this关键字

咱们知道,ES5函数中的this指向的是运行时所在的做用域。好比

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

var id = 21;

foo.call({id:42});//id: 21

在这里,我声明了一个函数foo,其内部为一个延迟函数setTimeout,每隔100ms打印一个this.id。咱们经过foo.call({id:42})来调用它,而且为这个函数设定做用域。它真正执行要等到100毫秒后,因为this指向的是运行时所在的做用域,因此这里的this就指向了全局对象window,而不是函数foo。这里:

  • 使用call来改变foo的执行上下文,使函数的执行上下文再也不是window,从而来辨别setTimeout中的this指向

  • setTimeout方法挂在window对象下,因此其this指向执行时所在的做用域——window对象。

    超时调用的代码都是在全局做用域中执行的,所以函数中this 的值在非严格模式下指向window 对象,在严格模式下是undefined --《javascript高级程序设计》

为了解决这一问题,咱们往常的作法每每是将this赋值给其余变量:

function foo() {
      var that = this;
  setTimeout(function(){
    console.log('id:', that.id);
  }, 100);
}
    
var id = 21;
foo.call({id:42});//id: 42

而如今ES6推出了箭头函数解决了这一问题。

箭头函数

标识符=> 表达式

var sum = (num1, num2) => { return num1 + num2; }
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};
  • 若是函数只有一个参数,则能够省略圆括号

  • 若是函数只有一条返回语句,则能够省略大括号return

  • 若是函数直接返回一个对象,必须在对象外面加上括号。(由于一个空对象{}和一个空的块 {} 看起来彻底同样。因此须要用小括号包裹对象字面量。)

针对this关键字的问题,ES6规定箭头函数中的this绑定定义时所在的做用域,而不是指向运行时所在的做用域。这一以来,this指向固定化了,从而有利于封装回调函数。

function foo() {var that = this;
  setTimeout(()=>{
    console.log('id:', that.id);
  }, 100);
}
    
var id = 21;
foo.call({id:42});//id: 42

注意:箭头函数this指向的固定化,并非由于箭头函数内部有绑定this的机制,实际缘由是箭头函数根本没有本身的this。而箭头函数根本没有本身的this,其内部的this也就是外层代码块的this。这就致使了其:

  • 不能用做构造函数

  • 不能用call()、apply()、bind()这些方法去改变this的指向

类与继承

传统ECMAScript没类的概念,它描述了原型链的概念,并将原型链做为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。而实现这一行为的传统方法即是经过构造函数:

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

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

var p = new Point(1, 2);

在这里,构造函数Point会有一个原型对象(prototype),这个原型对象包含一个指向Point的指针(constructor),而实例p包含一个指向原型对象的内部指针(prop)。因此整个的继承是经过原型链来实现的。详情可见个人这篇文章:javascript中的prototype和constructor

class

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,做为对象的模板。经过class关键字,能够定义类。可是类只是基于原型的面向对象模式的语法糖。对于class的引入,褒贬不一,不少人认为它反而是一大缺陷,但对我来讲,这是一个好的语法糖,由于往常的原型链继承的方式每每能把我绕那么一下子。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
var p = new Point(1, 2);
  • 类里面有一个constructor方法,它是类的默认方法,经过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,若是没有显式定义,一个空的constructor方法会被默认添加。

  • constructor方法中的this关键字表明实例对象,

  • 定义“类”的方法(如上例的toString)的时候,前面不须要加上function这个关键字,直接把函数定义放进去了就能够了。另外,方法之间不须要逗号分隔,加了会报错。

  • 使用的时候,也是直接对类使用new命令,跟构造函数的用法彻底一致

  • 类的全部方法都定义在类的prototype属性上面

class的继承——extend

Class之间能够经过extends关键字实现继承,这比ES5的经过修改原型链实现继承,要清晰和方便不少。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}
  • super关键字,做为函数调用时(即super(...args)),它表明父类的构造函数;做为对象调用时(即super.prop或super.method()),它表明父类。在这里,它表示父类的构造函数,用来新建父类的this对象。
  • 子类必须在constructor方法中调用super方法,不然新建实例时会报错。这是由于子类没有本身的this对象,而是继承父类的this对象,而后对其进行加工。若是不调用super方法,子类就得不到this对象。

模块化

历史上,JavaScript一直没有模块(module)体系,没法将一个大程序拆分红互相依赖的小文件,再用简单的方法拼装起来,这对开发大型的、复杂的项目造成了巨大障碍。为了适应大型模块的开发,社区制定了一些模块加载方案,好比CMD和AMD。

ES6的模块化写法:

import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载3个方法,其余方法不加载。这种加载称为“编译时加载”,即ES6能够在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。固然,这也致使了无法引用ES6模块自己,由于它不是对象。

模块功能主要由两个命令构成:

  • export

    用于规定模块的对外接口,对外的接口,必须与模块内部的变量创建一一对应关系。

    // 写法一
      export var m = 1;
      //错误
      export 1;
    
      // 写法二
      var m = 1;
      export {m};
      //错误
      export m;
    
      // 写法三  重命名
      var n = 1;
      export {n as m};
  • import

    用于输入其余模块提供的功能,它接受一个对象(用大括号表示),里面指定要从其余模块导入的变量名(也可使用*号总体加载)

字符串插值

在javascript的开发中,咱们经常须要这样来输出模板:

function sayHello(name){
    return "hello,my name is "+name+" I am "+getAge(18);
}
function getAge(age){
    return age;
}
sayHello("brand") //"hello,my name is brand I am 18"

咱们须要使用+来链接字符串和变量(或者表达式)。例子比较简单,因此看上去无伤大雅,可是一旦在比较复杂的状况下,就会显得至关繁琐不方便,这一用法也让咱们不厌其烦。对此,ES6引入了模板字符串,能够方便优雅地将 JS 的值插入到字符串中。

模板字符串

对于模板字符串,它:

  • 使用反引号``包裹;
  • 使用${}来输出值;
  • ${}里的内容能够是任何 JavaScript 表达式,因此函数调用和算数运算等都是合法的;
  • 若是一个值不是字符串,它将被转换为字符串;
  • 保留全部的空格、换行和缩进,并输出到结果字符串中(能够书写多行字符串)
  • 内部使用反引号和大括号须要转义,转义使用反斜杠\

对于上面的例子,模板字符串的写法是:

function sayHello(name){
    return `hello,my name is ${name} I am ${getAge(18)}`;
}
function getAge(age){
    return age;
}
sayHello("brand") //"hello,my name is brandI am 18"

严格模式

严格模式的目标之一是容许更快地调试错误。帮助开发者调试的最佳途径是当肯定的问题发生时抛出相应的错误(throw errors when certain patterns occur),而不是悄无声息地失败或者表现出奇怪的行为(非严格模式下常常发生)。严格模式下的代码会抛出更多的错误信息,能帮助开发者很快注意到一些必须当即解决的问题。在 ES5 中, 严格模式是可选项,可是在 ES6 中,许多特性要求必须使用严格模式,这个习惯有助于咱们书写更好的 JavaScript。