ES6筆記(一):ES6所改良的javascript“缺陷”


塊級作用域

ES5沒有塊級作用域,只有全局作用域和函數作用域,由於這一點,變量的作用域甚廣,所以一進入函數就要馬上將它創建出來。這就造成了所謂的變量提升。

ES5的“變量提升”這一特性往往一不小心就會造成一下錯誤:

  1. 內層變量覆蓋外層變量

     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增加了塊級作用域,所以不再需要自執行函數了。

let 和 const

ES6是是向后兼容的,而保持向后兼容性意味着永不改變JS代碼在Web平台上的行為,所以var創建的變量其作用域依舊將會是全局作用域和函數作用域。這樣以來,即使擁有了塊級作用域,也無法解決ES5的“變量提升”問題。所以,這里ES6新增了倆個新關鍵詞:letconst

  1. let

    “let是更完美的var”,它有着更好的作用域規則。

  2. const
    const聲明一個只讀的常量。一旦聲明,常量的值就不能改變,但const聲明的對象可以有屬性變化(對象凍結Object.freeze)

     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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM