ES6 let 與 const 命令 以及箭頭函數初步學習
let 與 const
let
let 和const用于定義變量。let類似于之前的var。const是定義常量的。
ES6 新增了let命令,用來聲明變量。它的用法類似于var,但是所聲明的變量,只在let命令所在的代碼塊內有效。
<script>
{
var a = 12;
let b =10;
}
console.log(a)
console.log(b)
</script>
let使用在for循環中
//let使用for循環參數里只能在循環內部使用
for(let i=0;i<=5;i++){
console.log(i)
}
console.log(i) //not defined
let在for循環添加事件中最典型的使用方式
var btns = document.getElementsByTagName('button');
for(let i=0;i<btns.length;i++){
btns[i].onclick = function(){
console.log(i)
}
}
let 在for循環作為循環參數的時候,其實是倆個作用域,大括號里面的是子作用域,如果里面再次定義與循環參數相同的變量,則不會產生相互影響。
//for其實是倆個作用域,大括號里面是一個子作用域
for(let i=0;i<=3;i++){
let i = 'a';
console.log(i);
}
let不存在變量提升,let命令改變了語法行為,它所聲明的變量一定要在聲明后使用,否則報錯。
console.log(a) //not defined
let a = 12;
暫時性死區
ES6 明確規定,如果區塊中存在let和const命令,這個區塊對這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。
在代碼塊內,使用let命令聲明變量之前,該變量都是不可用的。這在語法上,稱為“暫時性死區“,let不允許重復定義一個變量。
//大括號括起來的稱之為塊作用域,有循環 switch結構 if結構
for(let i =0;i<3;i++){
console.log(i)
// let i ='a';
let i = '123';
}
塊級作用于
-
ES5 規定,函數只能在頂層作用域和函數作用域之中聲明,不能在塊級作用域聲明。
瀏覽器沒有遵守這個規定,為了兼容以前的舊代碼,還是支持在塊級作用域之中聲明函數,因此上面兩種情況實際都能運行,不會報錯。
if(true){
function say(){
console.log(1)
}
}
say(); //1
- ES6 引入了塊級作用域,明確允許在塊級作用域之中聲明函數。ES6 規定,塊級作用域之中,函數聲明語句的行為類似于let,在塊級作用域之外不可引用。
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
if(true){
function say(){
console.log(1)
}
}
say();
</script>
為了減輕因此產生的不兼容問題,ES6 在附錄 B里面規定,瀏覽器的實現可以不遵守上面的規定,有自己的行為方式。
- 允許在塊級作用域內聲明函數。
- 函數聲明類似于var,即會提升到全局作用域或函數作用域的頭部。
- 同時,函數聲明還會提升到所在的塊級作用域的頭部。
const
-
const
聲明一個只讀的常量。一旦聲明,常量的值就不能改變。
const PI = 3.1415;
PI // 3.1415
PI = 3;
console.log(PI);//TypeError: invalid assignment to const `PI'
上面代碼表明改變常量的值會報錯。
-
const
聲明的變量不得改變值,這意味著,const
一旦聲明變量,就必須立即初始化,不能留到以后賦值。
const foo;
// SyntaxError: Missing initializer in const declaration
上面代碼表示,對于const
來說,只聲明不賦值,就會報錯。
-
const
的作用域與let
命令相同:只在聲明所在的塊級作用域內有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
-
const
命令聲明的常量也是不提升,同樣存在暫時性死區,只能在聲明的位置后面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
上面代碼在常量MAX
聲明之前就調用,結果報錯。
-
const
聲明的常量,也與let
一樣不可重復聲明。
var message = "Hello!";
let age = 25;
// 以下兩行都會報錯
const message = "Goodbye!";
const age = 30;
本質
-
const
實際上保證的,并不是變量的值不得改動,而是變量指向的那個內存地址所保存的數據不得改動。對于簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,因此等同于常量。但對于復合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指向實際數據的指針,const
只能保證這個指針是固定的(即總是指向另一個固定的地址),至于它指向的數據結構是不是可變的,就完全不能控制了。因此,將一個對象聲明為常量必須非常小心。
const foo = {};
// 為 foo 添加一個屬性,可以成功
foo.prop = 123;
console.log(foo.prop); // 123
// 將 foo 指向另一個對象,就會報錯
foo = {}; // TypeError: "foo" is read-only
// "foo" is read-only
上面代碼中,常量foo
儲存的是一個地址,這個地址指向一個對象。不可變的只是這個地址,即不能把foo
指向另一個地址,但對象本身是可變的,所以依然可以為其添加新屬性。
const a = [];
a.push('Hello'); // 可執行
console.log(a); // Array ["Hello"]
a.length = 0; // 可執行
console.log(a); // Array []
a = ['Dave']; // 報錯 TypeError: invalid assignment to const `a'
上面代碼中,常量a
是一個數組,這個數組本身是可寫的,但是如果將另一個數組賦值給a
,就會報錯。
如果真的想將對象凍結,應該使用Object.freeze
方法。
const foo = Object.freeze({});
console.log(foo);
// 常規模式時,下面一行不起作用;
// 嚴格模式時,該行會報錯
foo.prop = 123;
console.log(foo); // Object { }
上面代碼中,常量foo
指向一個凍結的對象,所以添加新屬性不起作用,嚴格模式時還會報錯。
除了將對象本身凍結,對象的屬性也應該凍結。下面是一個將對象徹底凍結的函數。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
//這里用到了箭頭函數
ES6 聲明變量的六種方法
- ES5 只有兩種聲明變量的方法:
var
命令和function
命令。ES6 除了添加let
和const
命令,后面章節還會提到,另外兩種聲明變量的方法:import
命令和class
命令。所以,ES6 一共有 6 種聲明變量的方法。
ES6箭頭函數、箭頭函數與普通函數的區別
一、基本語法
ES6中允許使用箭頭=>
來定義箭頭函數,具體語法,我們來看一個簡單的例子:
// 箭頭函數
let fun = (name) => {
// 函數體
return `Hello ${name} !`;
};
// 等同于
let fun = function (name) {
// 函數體
return `Hello ${name} !`;
};
可以看出,定義箭頭函在數語法上要比普通函數簡潔得多。箭頭函數省去了function
關鍵字,采用箭頭=>
來定義函數。函數的參數放在=>
前面的括號中,函數體跟在=>
后的花括號中。
關于箭頭函數的參數:
① 如果箭頭函數沒有參數,直接寫一個空括號即可。
② 如果箭頭函數的參數只有一個,也可以省去包裹參數的括號。
③ 如果箭頭函數有多個參數,將參數依次用逗號(,)分隔,包裹在括號中即可。
// 沒有參數
let fun1 = () => {
console.log(111);
};
// 只有一個參數,可以省去參數括號
let fun2 = name => {
console.log(`Hello ${name} !`)
};
// 有多個參數
let fun3 = (val1, val2, val3) => {
return [val1, val2, val3];
};
關于箭頭函數的函數體:
①如果箭頭函數的函數體只有一句代碼,就是簡單返回某個變量或者返回一個簡單的JS表達式,可以省去函數體的大括號{ }。
let f = val => val;
// 等同于
let f = function (val) { return val };
let sum = (num1, num2) => num1 + num2;
// 等同于
let sum = function(num1, num2) {
return num1 + num2;
};
②如果箭頭函數的函數體只有一句代碼,就是返回一個對象,可以像下面這樣寫:
// 用小括號包裹要返回的對象,不報錯
let getTempItem = id => ({ id: id, name: "Temp" });
// 但絕不能這樣寫,會報錯。
// 因為對象的大括號會被解釋為函數體的大括號
let getTempItem = id => { id: id, name: "Temp" };
③如果箭頭函數的函數體只有一條語句并且不需要返回值(最常見是調用一個函數),可以給這條語句前面加一個void
關鍵字
let fn = () => void doesNotReturn();
箭頭函數最常見的用處就是簡化回調函數。
// 例子一
// 正常函數寫法
[1,2,3].map(function (x) {
return x * x;
});
// 箭頭函數寫法
[1,2,3].map(x => x * x);
// 例子二
// 正常函數寫法
var result = [2, 5, 1, 4, 3].sort(function (a, b) {
return a - b;
});
// 箭頭函數寫法
var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);
二、箭頭函數與普通函數的區別
1、語法更加簡潔、清晰
從上面的基本語法示例中可以看出,箭頭函數的定義要比普通函數定義簡潔、清晰得多,很快捷。
2、箭頭函數不會創建自己的this(重要!!深入理解!!)
我們先來看看MDN上對箭頭函數this
的解釋。
箭頭函數不會創建自己的this,所以它沒有自己的this,它只會從自己的作用域鏈的上一層繼承this。
箭頭函數沒有自己的this
,它會捕獲自己在定義時(注意,是定義時,不是調用時)所處的外層執行環境的this
,并繼承這個this
值。所以,箭頭函數中this
的指向在它被定義的時候就已經確定了,之后永遠不會改變。
來看個例子:
var id = 'Global';
function fun1() {
// setTimeout中使用普通函數
setTimeout(function(){
console.log(this.id);
}, 2000);
}
function fun2() {
// setTimeout中使用箭頭函數
setTimeout(() => {
console.log(this.id);
}, 2000)
}
fun1.call({id: 'Obj'}); // 'Global'
fun2.call({id: 'Obj'}); // 'Obj'
上面這個例子,函數fun1
中的setTimeout
中使用普通函數,2秒后函數執行時,這時函數其實是在全局作用域執行的,所以this
指向Window
對象,this.id
就指向全局變量id
,所以輸出'Global'
。
但是函數fun2
中的setTimeout
中使用的是箭頭函數,這個箭頭函數的this
在定義時就確定了,它繼承了它外層fun2
的執行環境中的this
,而fun2
調用時this
被call
方法改變到了對象{id: 'Obj'}
中,所以輸出'Obj'
。
再來看另一個例子:
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
上面這個例子,對象obj
的方法a
使用普通函數定義的,普通函數作為對象的方法調用時,this
指向它所屬的對象。所以,this.id
就是obj.id
,所以輸出'OBJ'
。
但是方法b
是使用箭頭函數定義的,箭頭函數中的this
實際是繼承的它定義時所處的全局執行環境中的this
,所以指向Window
對象,所以輸出'GLOBAL'
。(這里要注意,定義對象的大括號{}
是無法形成一個單獨的執行環境的,它依舊是處于全局執行環境中!!)
三、箭頭函數繼承而來的this指向永遠不變(重要!!深入理解!!)
上面的例子,就完全可以說明箭頭函數繼承而來的this
指向永遠不變。對象obj
的方法b
是使用箭頭函數定義的,這個函數中的this
就永遠指向它定義時所處的全局執行環境中的this
,即便這個函數是作為對象obj
的方法調用,this
依舊指向Window
對象。
四、.call()/.apply()/.bind()無法改變箭頭函數中this的指向
.call()
/.apply()
/.bind()
方法可以用來動態修改函數執行時this
的指向,但由于箭頭函數的this
定義時就已經確定且永遠不會改變。所以使用這些方法永遠也改變不了箭頭函數this
的指向,雖然這么做代碼不會報錯。
var id = 'Global';
// 箭頭函數定義在全局作用域
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
// this的指向不會改變,永遠指向Window對象
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
五、箭頭函數不能作為構造函數使用
我們先了解一下構造函數的new都做了些什么?簡單來說,分為四步:
① JS內部首先會先生成一個對象;
② 再把函數中的this指向該對象;
③ 然后執行構造函數中的語句;
④ 最終返回該對象實例。
但是!!因為箭頭函數沒有自己的this
,它的this
其實是繼承了外層執行環境中的this
,且this
指向永遠不會隨在哪里調用、被誰調用而改變,所以箭頭函數不能作為構造函數使用,或者說構造函數不能定義成箭頭函數,否則用new
調用時會報錯!
let Fun = (name, age) => {
this.name = name;
this.age = age;
};
// 報錯
let p = new Fun('cao', 24);
六、箭頭函數沒有自己的arguments
箭頭函數沒有自己的arguments
對象。在箭頭函數中訪問arguments
實際上獲得的是外層局部(函數)執行環境中的值。
// 例子一
let fun = (val) => {
console.log(val); // 111
// 下面一行會報錯
// Uncaught ReferenceError: arguments is not defined
// 因為外層全局環境沒有arguments對象
console.log(arguments);
};
fun(111);
// 例子二
function outer(val1, val2) {
let argOut = arguments;
console.log(argOut); // ①
let fun = () => {
let argIn = arguments;
console.log(argIn); // ②
console.log(argOut === argIn); // ③
};
fun();
}
outer(111, 222);
上面例子二,①②③處的輸出結果如下:
很明顯,普通函數outer
內部的箭頭函數fun
中的arguments
對象,其實是沿作用域鏈向上訪問的外層outer
函數的arguments
對象。
可以在箭頭函數中使用rest參數代替arguments對象,來訪問箭頭函數的參數列表!!
七、箭頭函數沒有原型prototype
let sayHi = () => {
console.log('Hello World !')
};
console.log(sayHi.prototype); // undefined