JavaScript共有八種數(shù)據(jù)類(lèi)型,分別是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是ES6 中新增的數(shù)據(jù)類(lèi)型:
這些數(shù)據(jù)可以分為原始數(shù)據(jù)類(lèi)型和引用數(shù)據(jù)類(lèi)型:
兩種類(lèi)型的區(qū)別在于存儲(chǔ)位置的不同:
堆和棧的概念存在于數(shù)據(jù)結(jié)構(gòu)和操作系統(tǒng)內(nèi)存中,在數(shù)據(jù)結(jié)構(gòu)中:
在操作系統(tǒng)中,內(nèi)存被分為棧區(qū)和堆區(qū):
(1)typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
其中數(shù)組、對(duì)象、null都會(huì)被判斷為object,其他判斷都正確。
(2)instanceof
instanceof
可以正確判斷對(duì)象的類(lèi)型,其內(nèi)部運(yùn)行機(jī)制是判斷在其原型鏈中能否找到該類(lèi)型的原型。
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看到,instanceof
只能正確判斷引用數(shù)據(jù)類(lèi)型,而不能判斷基本數(shù)據(jù)類(lèi)型。instanceof
運(yùn)算符可以用來(lái)測(cè)試一個(gè)對(duì)象在其原型鏈中是否存在一個(gè)構(gòu)造函數(shù)的 prototype
屬性。
(3) constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
constructor
有兩個(gè)作用,一是判斷數(shù)據(jù)的類(lèi)型,二是對(duì)象實(shí)例通過(guò) constrcutor
對(duì)象訪問(wèn)它的構(gòu)造函數(shù)。需要注意,如果創(chuàng)建一個(gè)對(duì)象來(lái)改變它的原型,constructor
就不能用來(lái)判斷數(shù)據(jù)類(lèi)型了:
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
(4)Object.prototype.toString.call()
Object.prototype.toString.call()
使用 Object 對(duì)象的原型方法 toString 來(lái)判斷數(shù)據(jù)類(lèi)型:
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
同樣是檢測(cè)對(duì)象obj調(diào)用toString方法,obj.toString()的結(jié)果和Object.prototype.toString.call(obj)的結(jié)果不一樣,這是為什么?
這是因?yàn)閠oString是Object的原型方法,而Array、function等類(lèi)型作為Object的實(shí)例,都重寫(xiě)了toString方法。不同的對(duì)象類(lèi)型調(diào)用toString方法時(shí),根據(jù)原型鏈的知識(shí),調(diào)用的是對(duì)應(yīng)的重寫(xiě)之后的toString方法(function類(lèi)型返回內(nèi)容為函數(shù)體的字符串,Array類(lèi)型返回元素組成的字符串…),而不會(huì)去調(diào)用Object上原型toString方法(返回對(duì)象的具體類(lèi)型),所以采用obj.toString()不能得到其對(duì)象類(lèi)型,只能將obj轉(zhuǎn)換為字符串類(lèi)型;因此,在想要得到對(duì)象的具體類(lèi)型時(shí),應(yīng)該調(diào)用Object原型上的toString方法。
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
obj.__proto__ === Array.prototype;
Array.isArrray(obj);
obj instanceof Array
Array.prototype.isPrototypeOf(obj)
首先 Undefined 和 Null 都是基本數(shù)據(jù)類(lèi)型,這兩個(gè)基本數(shù)據(jù)類(lèi)型分別都只有一個(gè)值,就是 undefined 和 null。
undefined 代表的含義是未定義,null 代表的含義是空對(duì)象。一般變量聲明了但還沒(méi)有定義的時(shí)候會(huì)返回 undefined,null主要用于賦值給一些可能會(huì)返回對(duì)象的變量,作為初始化。
undefined 在 JavaScript 中不是一個(gè)保留字,這意味著可以使用 undefined 來(lái)作為一個(gè)變量名,但是這樣的做法是非常危險(xiǎn)的,它會(huì)影響對(duì) undefined 值的判斷。我們可以通過(guò)一些方法獲得安全的 undefined 值,比如說(shuō) void 0。
當(dāng)對(duì)這兩種類(lèi)型使用 typeof 進(jìn)行判斷時(shí),Null 類(lèi)型化會(huì)返回 “object”,這是一個(gè)歷史遺留的問(wèn)題。當(dāng)使用雙等號(hào)對(duì)兩種類(lèi)型的值進(jìn)行比較時(shí)會(huì)返回 true,使用三個(gè)等號(hào)時(shí)會(huì)返回 false。
typeof null 的結(jié)果是Object。
在 JavaScript 第一個(gè)版本中,所有值都存儲(chǔ)在 32 位的單元中,每個(gè)單元包含一個(gè)小的 類(lèi)型標(biāo)簽(1-3 bits) 以及當(dāng)前要存儲(chǔ)值的真實(shí)數(shù)據(jù)。類(lèi)型標(biāo)簽存儲(chǔ)在每個(gè)單元的低位中,共有五種數(shù)據(jù)類(lèi)型:
000: object - 當(dāng)前存儲(chǔ)的數(shù)據(jù)指向一個(gè)對(duì)象。
1: int - 當(dāng)前存儲(chǔ)的數(shù)據(jù)是一個(gè) 31 位的有符號(hào)整數(shù)。
010: double - 當(dāng)前存儲(chǔ)的數(shù)據(jù)指向一個(gè)雙精度的浮點(diǎn)數(shù)。
100: string - 當(dāng)前存儲(chǔ)的數(shù)據(jù)指向一個(gè)字符串。
110: boolean - 當(dāng)前存儲(chǔ)的數(shù)據(jù)是布爾值。
如果最低位是 1,則類(lèi)型標(biāo)簽標(biāo)志位的長(zhǎng)度只有一位;如果最低位是 0,則類(lèi)型標(biāo)簽標(biāo)志位的長(zhǎng)度占三位,為存儲(chǔ)其他四種數(shù)據(jù)類(lèi)型提供了額外兩個(gè) bit 的長(zhǎng)度。
有兩種特殊數(shù)據(jù)類(lèi)型:
那也就是說(shuō)null的類(lèi)型標(biāo)簽也是000,和Object的類(lèi)型標(biāo)簽一樣,所以會(huì)被判定為Object。
instanceof 運(yùn)算符用于判斷構(gòu)造函數(shù)的 prototype 屬性是否出現(xiàn)在對(duì)象的原型鏈中的任何位置。
function myInstanceof(left, right) {
// 獲取對(duì)象的原型
let proto = Object.getPrototypeOf(left)
// 獲取構(gòu)造函數(shù)的 prototype 對(duì)象
let prototype = right.prototype;
// 判斷構(gòu)造函數(shù)的 prototype 對(duì)象是否在對(duì)象的原型鏈上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果沒(méi)有找到,就繼續(xù)從其原型上找,Object.getPrototypeOf方法用來(lái)獲取指定對(duì)象的原型
proto = Object.getPrototypeOf(proto);
}
}
在開(kāi)發(fā)過(guò)程中遇到類(lèi)似這樣的問(wèn)題:
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
這里得到的不是想要的結(jié)果,要想等于0.3,就要把它進(jìn)行轉(zhuǎn)化:
(n1 + n2).toFixed(2) // 注意,toFixed為四舍五入
toFixed(num)
方法可把 Number 四舍五入為指定小數(shù)位數(shù)的數(shù)字。那為什么會(huì)出現(xiàn)這樣的結(jié)果呢?
計(jì)算機(jī)是通過(guò)二進(jìn)制的方式存儲(chǔ)數(shù)據(jù)的,所以計(jì)算機(jī)計(jì)算0.1+0.2的時(shí)候,實(shí)際上是計(jì)算的兩個(gè)數(shù)的二進(jìn)制的和。0.1的二進(jìn)制是 0.0001100110011001100...
(1100循環(huán)),0.2的二進(jìn)制是:0.00110011001100...
(1100循環(huán)),這兩個(gè)數(shù)的二進(jìn)制都是無(wú)限循環(huán)的數(shù)。那JavaScript是如何處理無(wú)限循環(huán)的二進(jìn)制小數(shù)呢?
一般我們認(rèn)為數(shù)字包括整數(shù)和小數(shù),但是在 JavaScript 中只有一種數(shù)字類(lèi)型:Number,它的實(shí)現(xiàn)遵循IEEE 754標(biāo)準(zhǔn),使用64位固定長(zhǎng)度來(lái)表示,也就是標(biāo)準(zhǔn)的double雙精度浮點(diǎn)數(shù)。在二進(jìn)制科學(xué)表示法中,雙精度浮點(diǎn)數(shù)的小數(shù)部分最多只能保留52位,再加上前面的1,其實(shí)就是保留53位有效數(shù)字,剩余的需要舍去,遵從“0舍1入”的原則。
根據(jù)這個(gè)原則,0.1和0.2的二進(jìn)制數(shù)相加,再轉(zhuǎn)化為十進(jìn)制數(shù)就是:0.30000000000000004
。
下面看一下雙精度數(shù)是如何保存的:
對(duì)于0.1,它的二進(jìn)制為:
0.00011001100110011001100110011001100110011001100110011001 10011...
轉(zhuǎn)為科學(xué)計(jì)數(shù)法(科學(xué)計(jì)數(shù)法的結(jié)果就是浮點(diǎn)數(shù)):
1.1001100110011001100110011001100110011001100110011001*2^-4
可以看出0.1的符號(hào)位為0,指數(shù)位為-4,小數(shù)位為:
1001100110011001100110011001100110011001100110011001
那么問(wèn)題又來(lái)了,指數(shù)位是負(fù)數(shù),該如何保存呢?
IEEE標(biāo)準(zhǔn)規(guī)定了一個(gè)偏移量,對(duì)于指數(shù)部分,每次都加這個(gè)偏移量進(jìn)行保存,這樣即使指數(shù)是負(fù)數(shù),那么加上這個(gè)偏移量也就是正數(shù)了。由于JavaScript的數(shù)字是雙精度數(shù),這里就以雙精度數(shù)為例,它的指數(shù)部分為11位,能表示的范圍就是0~2047,IEEE固定雙精度數(shù)的偏移量為1023。
對(duì)于上面的0.1的指數(shù)位為-4,-4+1023 = 1019 轉(zhuǎn)化為二進(jìn)制就是:1111111011
.
所以,0.1表示為:
0 1111111011 1001100110011001100110011001100110011001100110011001
說(shuō)了這么多,是時(shí)候該最開(kāi)始的問(wèn)題了,如何實(shí)現(xiàn)0.1+0.2=0.3呢?
對(duì)于這個(gè)問(wèn)題,一個(gè)直接的解決方法就是設(shè)置一個(gè)誤差范圍,通常稱(chēng)為“機(jī)器精度”。對(duì)JavaScript來(lái)說(shuō),這個(gè)值通常為2-52,在ES6中,提供了 Number.EPSILON
屬性,而它的值就是2-52,只要判斷 0.1+0.2-0.3
是否小于 Number.EPSILON
,如果小于,就可以判斷為0.1+0.2 ===0.3
function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
因?yàn)?undefined 是一個(gè)標(biāo)識(shí)符,所以可以被當(dāng)作變量來(lái)使用和賦值,但是這樣會(huì)影響 undefined 的正常判斷。表達(dá)式 void ___ 沒(méi)有返回值,因此返回結(jié)果是 undefined。void 并不改變表達(dá)式的結(jié)果,只是讓表達(dá)式不返回值。因此可以用 void 0 來(lái)獲得 undefined。
NaN 指“不是一個(gè)數(shù)字”(not a number),NaN 是一個(gè)“警戒值”(sentinel value,有特殊用途的常規(guī)值),用于指出數(shù)字類(lèi)型中的錯(cuò)誤情況,即“執(zhí)行數(shù)學(xué)運(yùn)算沒(méi)有成功,這是失敗后返回的結(jié)果”。
typeof NaN; // "number"
NaN 是一個(gè)特殊值,它和自身不相等,是唯一一個(gè)非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 為 true。
對(duì)于 ==
來(lái)說(shuō),如果對(duì)比雙方的類(lèi)型不一樣,就會(huì)進(jìn)行類(lèi)型轉(zhuǎn)換。假如對(duì)比 x
和 y
是否相同,就會(huì)進(jìn)行如下判斷流程:
null
?和 ?undefined
?,是的話就會(huì)返回 ?true
?string
?和 ?number
?,是的話就會(huì)將字符串轉(zhuǎn)換為 ?number
?1 == '1'
↓
1 == 1
boolean
?,是的話就會(huì)把 ?boolean
?轉(zhuǎn)為 ?number
?再進(jìn)行判斷'1' == true
↓
'1' == 1
↓
1 == 1
object
?且另一方為 ?string
?、?number
?或者 ?symbol
?,是的話就會(huì)把 ?object
?轉(zhuǎn)為原始類(lèi)型再進(jìn)行判斷'1' == { name: 'js' }
↓
'1' == '[object Object]'
其流程圖如下:
為了將值轉(zhuǎn)換為相應(yīng)的基本類(lèi)型值,抽象操作 ToPrimitive 會(huì)首先(通過(guò)內(nèi)部操作 DefaultValue)檢查該值是否有valueOf()方法。如果有并且返回基本類(lèi)型值,就使用該值進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換。如果沒(méi)有就使用 toString() 的返回值(如果存在)來(lái)進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換。
如果 valueOf() 和 toString() 均不返回基本類(lèi)型值,會(huì)產(chǎn)生 TypeError 錯(cuò)誤。
以下這些是假值:
假值的布爾強(qiáng)制類(lèi)型轉(zhuǎn)換結(jié)果為 false。從邏輯上說(shuō),假值列表以外的都應(yīng)該是真值。
|| 和 && 首先會(huì)對(duì)第一個(gè)操作數(shù)執(zhí)行條件判斷,如果其不是布爾值就先強(qiáng)制轉(zhuǎn)換為布爾類(lèi)型,然后再執(zhí)行條件判斷。
|| 和 && 返回它們其中一個(gè)操作數(shù)的值,而非條件判斷的結(jié)果
在 JavaScript 中,基本類(lèi)型是沒(méi)有屬性和方法的,但是為了便于操作基本類(lèi)型的值,在調(diào)用基本類(lèi)型的屬性或方法時(shí) JavaScript 會(huì)在后臺(tái)隱式地將基本類(lèi)型的值轉(zhuǎn)換為對(duì)象,如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
在訪問(wèn) 'abc'.length
時(shí),JavaScript 將 'abc'
在后臺(tái)轉(zhuǎn)換成 String('abc')
,然后再訪問(wèn)其 length
屬性。
JavaScript也可以使用 Object
函數(shù)顯式地將基本類(lèi)型轉(zhuǎn)換為包裝類(lèi)型:
var a = 'abc'
Object(a) // String {"abc"}
也可以使用 valueOf
方法將包裝類(lèi)型倒轉(zhuǎn)成基本類(lèi)型:
var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
看看如下代碼會(huì)打印出什么:
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // never runs
}
答案是什么都不會(huì)打印,因?yàn)殡m然包裹的基本類(lèi)型是 false
,但是 false
被包裹成包裝類(lèi)型后就成了對(duì)象,所以其非值為 false
,所以循環(huán)體中的內(nèi)容不會(huì)運(yùn)行。
首先要介紹 ToPrimitive
方法,這是 JavaScript 中每個(gè)值隱含的自帶的方法,用來(lái)將值 (無(wú)論是基本類(lèi)型值還是對(duì)象)轉(zhuǎn)換為基本類(lèi)型值。如果值為基本類(lèi)型,則直接返回值本身;如果值為對(duì)象,其看起來(lái)大概是這樣:
/**
* @obj 需要轉(zhuǎn)換的對(duì)象
* @type 期望的結(jié)果類(lèi)型
*/
ToPrimitive(obj,type)
type
的值為 number
或者 string
。
(1)當(dāng) type
為 number
時(shí)規(guī)則如下:
obj
?的 ?valueOf
?方法,如果為原始值,則返回,否則下一步;obj
?的 ?toString
?方法,后續(xù)同上;TypeError
?異常。(2)當(dāng) type
為 string
時(shí)規(guī)則如下:
obj
?的 ?toString
?方法,如果為原始值,則返回,否則下一步;obj
?的 ?valueOf
?方法,后續(xù)同上;TypeError
?異常。可以看出兩者的主要區(qū)別在于調(diào)用 toString
和 valueOf
的先后順序。默認(rèn)情況下:
type
?默認(rèn)為 ?string
?;type
?默認(rèn)為 ?number
?。總結(jié)上面的規(guī)則,對(duì)于 Date 以外的對(duì)象,轉(zhuǎn)換為基本類(lèi)型的大概規(guī)則可以概括為一個(gè)函數(shù):
var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN
而 JavaScript 中的隱式類(lèi)型轉(zhuǎn)換主要發(fā)生在 +、-、*、/
以及 ==、>、<
這些運(yùn)算符之間。而這些運(yùn)算符只能操作基本類(lèi)型值,所以在進(jìn)行這些運(yùn)算前的第一步就是將兩邊的值用 ToPrimitive
轉(zhuǎn)換成基本類(lèi)型,再進(jìn)行操作。
以下是基本類(lèi)型的值在不同操作符的情況下隱式轉(zhuǎn)換的規(guī)則 (對(duì)于對(duì)象,其會(huì)被 ToPrimitive
轉(zhuǎn)換成基本類(lèi)型,所以最終還是要應(yīng)用基本類(lèi)型轉(zhuǎn)換規(guī)則):
1 + '23' // '123'
1 + false // 1
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
操作符兩邊的值都盡量轉(zhuǎn)成 number
:
3 == true // false, 3 轉(zhuǎn)為number為3,true轉(zhuǎn)為number為1
'0' == false //true, '0'轉(zhuǎn)為number為0,false轉(zhuǎn)為number為0
'0' == 0 // '0'轉(zhuǎn)為number為0
如果兩邊都是字符串,則比較字母表順序:
'ca' < 'bd' // false
'a' < 'b' // true
其他情況下,轉(zhuǎn)換為數(shù)字再比較:
'12' < 13 // true
false > -1 // true
以上說(shuō)的是基本類(lèi)型的隱式轉(zhuǎn)換,而對(duì)象會(huì)被 ToPrimitive
轉(zhuǎn)換為基本類(lèi)型再進(jìn)行轉(zhuǎn)換:
var a = {}
a > 2 // false
其對(duì)比過(guò)程如下:
a.valueOf() // {}, 上面提到過(guò),ToPrimitive默認(rèn)type為number,所以先valueOf,結(jié)果還是個(gè)對(duì)象,下一步
a.toString() // "[object Object]",現(xiàn)在是一個(gè)字符串了
Number(a.toString()) // NaN,根據(jù)上面 < 和 > 操作符的規(guī)則,要轉(zhuǎn)換成數(shù)字
NaN > 2 //false,得出比較結(jié)果
又比如:
var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"
運(yùn)算過(guò)程如下:
a.valueOf() // {},上面提到過(guò),ToPrimitive默認(rèn)type為number,所以先valueOf,結(jié)果還是個(gè)對(duì)象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"
根據(jù) ES5 規(guī)范,如果某個(gè)操作數(shù)是字符串或者能夠通過(guò)以下步驟轉(zhuǎn)換為字符串的話,+ 將進(jìn)行拼接操作。如果其中一個(gè)操作數(shù)是對(duì)象(包括數(shù)組),則首先對(duì)其調(diào)用 ToPrimitive 抽象操作,該抽象操作再調(diào)用 [[DefaultValue]],以數(shù)字作為上下文。如果不能轉(zhuǎn)換為字符串,則會(huì)將其轉(zhuǎn)換為數(shù)字類(lèi)型來(lái)進(jìn)行計(jì)算。
簡(jiǎn)單來(lái)說(shuō)就是,如果 + 的其中一個(gè)操作數(shù)是字符串(或者通過(guò)以上步驟最終得到字符串),則執(zhí)行字符串拼接,否則執(zhí)行數(shù)字加法。
那么對(duì)于除了加法的運(yùn)算符來(lái)說(shuō),只要其中一方是數(shù)字,那么另一方就會(huì)被轉(zhuǎn)為數(shù)字。
JavaScript中Number.MAX_SAFE_INTEGER表示最?安全數(shù)字,計(jì)算結(jié)果是9007199254740991,即在這個(gè)數(shù)范圍內(nèi)不會(huì)出現(xiàn)精度丟失(?數(shù)除外)。但是?旦超過(guò)這個(gè)范圍,js就會(huì)出現(xiàn)計(jì)算不準(zhǔn)確的情況,這在?數(shù)計(jì)算的時(shí)候不得不依靠?些第三?庫(kù)進(jìn)?解決,因此官?提出了BigInt來(lái)解決此問(wèn)題。
擴(kuò)展運(yùn)算符:
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
Object.assign():
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
(1)塊級(jí)作用域:塊作用域由 { }
包括,let和const具有塊級(jí)作用域,var不存在塊級(jí)作用域。塊級(jí)作用域解決了ES5中的兩個(gè)問(wèn)題:
(2)變量提升:var存在變量提升,let和const不存在變量提升,即在變量只能在聲明之后使用,否在會(huì)報(bào)錯(cuò)。
(3)給全局添加屬性:瀏覽器的全局對(duì)象是window,Node的全局對(duì)象是global。var聲明的變量為全局變量,并且會(huì)將該變量添加為全局對(duì)象的屬性,但是let和const不會(huì)。
(4)重復(fù)聲明:var聲明變量時(shí),可以重復(fù)聲明變量,后聲明的同名變量會(huì)覆蓋之前聲明的遍歷。const和let不允許重復(fù)聲明變量。
(5)暫時(shí)性死區(qū):在使用let、const命令聲明變量之前,該變量都是不可用的。這在語(yǔ)法上,稱(chēng)為暫時(shí)性死區(qū)。使用var聲明的變量不存在暫時(shí)性死區(qū)。
(6)初始值設(shè)置:在變量聲明時(shí),var 和 let 可以不用設(shè)置初始值。而const聲明變量必須設(shè)置初始值。
(7)指針指向:let和const都是ES6新增的用于創(chuàng)建變量的語(yǔ)法。 let創(chuàng)建的變量是可以更改指針指向(可以重新賦值)。但const聲明的變量是不允許改變指針的指向。
區(qū)別 | var | let | const |
---|---|---|---|
是否有塊級(jí)作用域 | × |
|
|
是否存在變量提升 |
|
× | × |
是否添加全局屬性 |
|
× | × |
能否重復(fù)聲明變量 |
|
× | × |
是否存在暫時(shí)性死區(qū) | × |
|
|
是否必須設(shè)置初始值 | × | × |
|
能否改變指針指向 |
|
|
× |
const保證的并不是變量的值不能改動(dòng),而是變量指向的那個(gè)內(nèi)存地址不能改動(dòng)。對(duì)于基本類(lèi)型的數(shù)據(jù)(數(shù)值、字符串、布爾值),其值就保存在變量指向的那個(gè)內(nèi)存地址,因此等同于常量。
但對(duì)于引用類(lèi)型的數(shù)據(jù)(主要是對(duì)象和數(shù)組)來(lái)說(shuō),變量指向數(shù)據(jù)的內(nèi)存地址,保存的只是一個(gè)指針,const只能保證這個(gè)指針是固定不變的,至于它指向的數(shù)據(jù)結(jié)構(gòu)是不是可變的,就完全不能控制了。
箭頭函數(shù)是ES6中的提出來(lái)的,它沒(méi)有prototype,也沒(méi)有自己的this指向,更不可以使用arguments參數(shù),所以不能New一個(gè)箭頭函數(shù)。
new操作符的實(shí)現(xiàn)步驟如下:
所以,上面的第二、三步,箭頭函數(shù)都是沒(méi)有辦法執(zhí)行的。
(1)箭頭函數(shù)比普通函數(shù)更加簡(jiǎn)潔
let fn = () => void doesNotReturn();
(2)箭頭函數(shù)沒(méi)有自己的this
箭頭函數(shù)不會(huì)創(chuàng)建自己的this, 所以它沒(méi)有自己的this,它只會(huì)在自己作用域的上一層繼承this。所以箭頭函數(shù)中this的指向在它在定義時(shí)已經(jīng)確定了,之后不會(huì)改變。
(3)箭頭函數(shù)繼承來(lái)的this指向永遠(yuǎn)不會(huì)改變
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
new obj.a() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor
對(duì)象obj的方法b是使用箭頭函數(shù)定義的,這個(gè)函數(shù)中的this就永遠(yuǎn)指向它定義時(shí)所處的全局執(zhí)行環(huán)境中的this,即便這個(gè)函數(shù)是作為對(duì)象obj的方法調(diào)用,this依舊指向Window對(duì)象。需要注意,定義對(duì)象的大括號(hào) {}
是無(wú)法形成一個(gè)單獨(dú)的執(zhí)行環(huán)境的,它依舊是處于全局執(zhí)行環(huán)境中。
(4)call()、apply()、bind()等方法不能改變箭頭函數(shù)中this的指向
var id = 'Global';
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
(5)箭頭函數(shù)不能作為構(gòu)造函數(shù)使用
構(gòu)造函數(shù)在new的步驟在上面已經(jīng)說(shuō)過(guò)了,實(shí)際上第二步就是將函數(shù)中的this指向該對(duì)象。 但是由于箭頭函數(shù)時(shí)沒(méi)有自己的this的,且this指向外層的執(zhí)行環(huán)境,且不能改變指向,所以不能當(dāng)做構(gòu)造函數(shù)使用。
(6)箭頭函數(shù)沒(méi)有自己的arguments
箭頭函數(shù)沒(méi)有自己的arguments對(duì)象。在箭頭函數(shù)中訪問(wèn)arguments實(shí)際上獲得的是它外層函數(shù)的arguments值。
(7)箭頭函數(shù)沒(méi)有prototype
(8)箭頭函數(shù)不能用作Generator函數(shù),不能使用yeild關(guān)鍵字
箭頭函數(shù)不同于傳統(tǒng)JavaScript中的函數(shù),箭頭函數(shù)并沒(méi)有屬于??的this,它所謂的this是捕獲其所在上下?的 this 值,作為??的 this 值,并且由于沒(méi)有屬于??的this,所以是不會(huì)被new調(diào)?的,這個(gè)所謂的this也不會(huì)被改變。
可以?Babel理解?下箭頭函數(shù):
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
}
轉(zhuǎn)化后:
// ES5,由 Babel 轉(zhuǎn)譯
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};
(1)對(duì)象擴(kuò)展運(yùn)算符
對(duì)象的擴(kuò)展運(yùn)算符(...)用于取出參數(shù)對(duì)象中的所有可遍歷屬性,拷貝到當(dāng)前對(duì)象之中。
let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }
上述方法實(shí)際上等價(jià)于:
let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }
Object.assign
方法用于對(duì)象的合并,將源對(duì)象 (source)
的所有可枚舉屬性,復(fù)制到目標(biāo)對(duì)象 (target)
。Object.assign
方法的第一個(gè)參數(shù)是目標(biāo)對(duì)象,后面的參數(shù)都是源對(duì)象。(如果目標(biāo)對(duì)象與源對(duì)象有同名屬性,或多個(gè)源對(duì)象有同名屬性,則后面的屬性會(huì)覆蓋前面的屬性)。
同樣,如果用戶自定義的屬性,放在擴(kuò)展運(yùn)算符后面,則擴(kuò)展運(yùn)算符內(nèi)部的同名屬性會(huì)被覆蓋掉。
let bar = {a: 1, b: 2};
let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}
利用上述特性就可以很方便的修改對(duì)象的部分屬性。在 redux
中的 reducer
函數(shù)規(guī)定必須是一個(gè)純函數(shù),reducer
中的 state
對(duì)象要求不能直接修改,可以通過(guò)擴(kuò)展運(yùn)算符把修改路徑的對(duì)象都復(fù)制一遍,然后產(chǎn)生一個(gè)新的對(duì)象返回。
需要注意:擴(kuò)展運(yùn)算符對(duì)對(duì)象實(shí)例的拷貝屬于淺拷貝。
(2)數(shù)組擴(kuò)展運(yùn)算符
數(shù)組的擴(kuò)展運(yùn)算符可以將一個(gè)數(shù)組轉(zhuǎn)為用逗號(hào)分隔的參數(shù)序列,且每次只能展開(kāi)一層數(shù)組。
console.log(...[1, 2, 3])
// 1 2 3
console.log(...[1, [2, 3, 4], 5])
// 1 [2, 3, 4] 5
下面是數(shù)組的擴(kuò)展運(yùn)算符的應(yīng)用:
function add(x, y) {
return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
const arr1 = [1, 2];
const arr2 = [...arr1];
const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];
// ["one", "two", "three", "four", "five"]
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
需要注意:如果將擴(kuò)展運(yùn)算符用于數(shù)組賦值,只能放在參數(shù)的最后一位,否則會(huì)報(bào)錯(cuò)。
const [...rest, last] = [1, 2, 3, 4, 5]; // 報(bào)錯(cuò)
const [first, ...rest, last] = [1, 2, 3, 4, 5]; // 報(bào)錯(cuò)
[...'hello'] // [ "h", "e", "l", "l", "o" ]
比較常見(jiàn)的應(yīng)用是可以將某些數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)為數(shù)組:
// arguments對(duì)象
function foo() {
const args = [...arguments];
}
用于替換 es5
中的 Array.prototype.slice.call(arguments)
寫(xiě)法。
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9
在 Vue3.0 中通過(guò) Proxy
來(lái)替換原本的 Object.defineProperty
來(lái)實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式。
Proxy 是 ES6 中新增的功能,它可以用來(lái)自定義對(duì)象中的操作。
let p = new Proxy(target, handler)
target
代表需要添加代理的對(duì)象,handler
用來(lái)自定義對(duì)象中的操作,比如可以用來(lái)自定義 set
或者 get
函數(shù)。
下面來(lái)通過(guò) Proxy
來(lái)實(shí)現(xiàn)一個(gè)數(shù)據(jù)響應(yīng)式:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`監(jiān)聽(tīng)到屬性${property}改變?yōu)?{v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 監(jiān)聽(tīng)到屬性a改變
p.a // 'a' = 2
在上述代碼中,通過(guò)自定義 set
和 get
函數(shù)的方式,在原本的邏輯中插入了我們的函數(shù)邏輯,實(shí)現(xiàn)了在對(duì)對(duì)象任何屬性進(jìn)行讀寫(xiě)時(shí)發(fā)出通知。
當(dāng)然這是簡(jiǎn)單版的響應(yīng)式實(shí)現(xiàn),如果需要實(shí)現(xiàn)一個(gè) Vue 中的響應(yīng)式,需要在 get
中收集依賴(lài),在 set
派發(fā)更新,之所以 Vue3.0 要使用 Proxy
替換原本的 API 原因在于 Proxy
無(wú)需一層層遞歸為每個(gè)屬性添加代理,一次即可完成以上操作,性能上更好,并且原本的實(shí)現(xiàn)有一些數(shù)據(jù)更新不能監(jiān)聽(tīng)到,但是 Proxy
可以完美監(jiān)聽(tīng)到任何方式的數(shù)據(jù)改變,唯一缺陷就是瀏覽器的兼容性不好。
解構(gòu)是 ES6 提供的一種新的提取數(shù)據(jù)的模式,這種模式能夠從對(duì)象或數(shù)組里有針對(duì)性地拿到想要的數(shù)值。
1)數(shù)組的解構(gòu)
在解構(gòu)數(shù)組時(shí),以元素的位置為匹配條件來(lái)提取想要的數(shù)據(jù)的:
const [a, b, c] = [1, 2, 3]
最終,a、b、c分別被賦予了數(shù)組第0、1、2個(gè)索引位的值:
數(shù)組里的0、1、2索引位的元素值,精準(zhǔn)地被映射到了左側(cè)的第0、1、2個(gè)變量里去,這就是數(shù)組解構(gòu)的工作模式。還可以通過(guò)給左側(cè)變量數(shù)組設(shè)置空占位的方式,實(shí)現(xiàn)對(duì)數(shù)組中某幾個(gè)元素的精準(zhǔn)提取:
const [a,,c] = [1,2,3]
通過(guò)把中間位留空,可以順利地把數(shù)組第一位和最后一位的值賦給 a、c 兩個(gè)變量:
2)對(duì)象的解構(gòu)
對(duì)象解構(gòu)比數(shù)組結(jié)構(gòu)稍微復(fù)雜一些,也更顯強(qiáng)大。在解構(gòu)對(duì)象時(shí),是以屬性的名稱(chēng)為匹配條件,來(lái)提取想要的數(shù)據(jù)的?,F(xiàn)在定義一個(gè)對(duì)象:
const stu = {
name: 'Bob',
age: 24
}
假如想要解構(gòu)它的兩個(gè)自有屬性,可以這樣:
const { name, age } = stu
這樣就得到了 name 和 age 兩個(gè)和 stu 平級(jí)的變量:
注意,對(duì)象解構(gòu)嚴(yán)格以屬性名作為定位依據(jù),所以就算調(diào)換了 name 和 age 的位置,結(jié)果也是一樣的:
const { age, name } = stu
有時(shí)會(huì)遇到一些嵌套程度非常深的對(duì)象:
const school = {
classes: {
stu: {
name: 'Bob',
age: 24,
}
}
}
像此處的 name 這個(gè)變量,嵌套了四層,此時(shí)如果仍然嘗試?yán)戏椒▉?lái)提取它:
const { name } = school
顯然是不奏效的,因?yàn)?school 這個(gè)對(duì)象本身是沒(méi)有 name 這個(gè)屬性的,name 位于 school 對(duì)象的“兒子的兒子”對(duì)象里面。要想把 name 提取出來(lái),一種比較笨的方法是逐層解構(gòu):
const { classes } = school
const { stu } = classes
const { name } = stu
name // 'Bob'
但是還有一種更標(biāo)準(zhǔn)的做法,可以用一行代碼來(lái)解決這個(gè)問(wèn)題:
const { classes: { stu: { name } }} = school
console.log(name) // 'Bob'
可以在解構(gòu)出來(lái)的變量名右側(cè),通過(guò)冒號(hào)+{目標(biāo)屬性名}這種形式,進(jìn)一步解構(gòu)它,一直解構(gòu)到拿到目標(biāo)數(shù)據(jù)為止。
擴(kuò)展運(yùn)算符被用在函數(shù)形參上時(shí),它還可以把一個(gè)分離的參數(shù)序列整合成一個(gè)數(shù)組:
function mutiple(...args) {
let result = 1;
for (var val of args) {
result *= val;
}
return result;
}
mutiple(1, 2, 3, 4) // 24
這里,傳入 mutiple 的是四個(gè)分離的參數(shù),但是如果在 mutiple 函數(shù)里嘗試輸出 args 的值,會(huì)發(fā)現(xiàn)它是一個(gè)數(shù)組:
function mutiple(...args) {
console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]
這就是 … rest運(yùn)算符的又一層威力了,它可以把函數(shù)的多個(gè)入?yún)⑹諗窟M(jìn)一個(gè)數(shù)組里。這一點(diǎn)經(jīng)常用于獲取函數(shù)的多余參數(shù),或者像上面這樣處理函數(shù)參數(shù)個(gè)數(shù)不確定的情況。
ES6 提出了“模板語(yǔ)法”的概念。在 ES6 以前,拼接字符串是很麻煩的事情:
var name = 'css'
var career = 'coder'
var hobby = ['coding', 'writing']
var finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]
僅僅幾個(gè)變量,寫(xiě)了這么多加號(hào),還要時(shí)刻小心里面的空格和標(biāo)點(diǎn)符號(hào)有沒(méi)有跟錯(cuò)地方。但是有了模板字符串,拼接難度直線下降:
var name = 'css'
var career = 'coder'
var hobby = ['coding', 'writing']
var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`
字符串不僅更容易拼了,也更易讀了,代碼整體的質(zhì)量都變高了。這就是模板字符串的第一個(gè)優(yōu)勢(shì)——允許用${}的方式嵌入變量。但這還不是問(wèn)題的關(guān)鍵,模板字符串的關(guān)鍵優(yōu)勢(shì)有兩個(gè):
基于第一點(diǎn),可以在模板字符串里無(wú)障礙地直接寫(xiě) html 代碼:
let list = `
<ul>
<li>列表項(xiàng)1</li>
<li>列表項(xiàng)2</li>
</ul>
`;
console.log(message); // 正確輸出,不存在報(bào)錯(cuò)
基于第二點(diǎn),可以把一些簡(jiǎn)單的計(jì)算和調(diào)用丟進(jìn) ${} 來(lái)做:
function add(a, b) {
const finalString = `${a} + $ = ${a+b}`
console.log(finalString)
}
add(1, 2) // 輸出 '1 + 2 = 3'
除了模板語(yǔ)法外, ES6中還新增了一系列的字符串方法用于提升開(kāi)發(fā)效率:
const son = 'haha'
const father = 'xixi haha hehe'
father.includes(son) // true
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
const father = 'xixi haha hehe'
father.endsWith('hehe') // true
const sourceCode = 'repeat for 3 times;'
const repeated = sourceCode.repeat(3)
console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
new操作符的執(zhí)行過(guò)程:
(1)首先創(chuàng)建了一個(gè)新的空對(duì)象
(2)設(shè)置原型,將對(duì)象的原型設(shè)置為函數(shù)的 prototype 對(duì)象。
(3)讓函數(shù)的 this 指向這個(gè)對(duì)象,執(zhí)行構(gòu)造函數(shù)的代碼(為這個(gè)新對(duì)象添加屬性)
(4)判斷函數(shù)的返回值類(lèi)型,如果是值類(lèi)型,返回創(chuàng)建的對(duì)象。如果是引用類(lèi)型,就返回這個(gè)引用類(lèi)型的對(duì)象。
具體實(shí)現(xiàn):
function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 判斷參數(shù)是否是一個(gè)函數(shù)
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一個(gè)空對(duì)象,對(duì)象的原型為構(gòu)造函數(shù)的 prototype 對(duì)象
newObject = Object.create(constructor.prototype);
// 將 this 指向新建對(duì)象,并執(zhí)行函數(shù)
result = constructor.apply(newObject, arguments);
// 判斷返回對(duì)象
let flag = result && (typeof result === "object" || typeof result === "function");
// 判斷返回結(jié)果
return flag ? result : newObject;
}
// 使用方法
objectFactory(構(gòu)造函數(shù), 初始化參數(shù));
Map | Object | |
---|---|---|
意外的鍵 | Map默認(rèn)情況不包含任何鍵,只包含顯式插入的鍵。 | Object 有一個(gè)原型, 原型鏈上的鍵名有可能和自己在對(duì)象上的設(shè)置的鍵名產(chǎn)生沖突。 |
鍵的類(lèi)型 | Map的鍵可以是任意值,包括函數(shù)、對(duì)象或任意基本類(lèi)型。 | Object 的鍵必須是 String 或是Symbol。 |
鍵的順序 | Map 中的 key 是有序的。因此,當(dāng)?shù)臅r(shí)候, Map 對(duì)象以插入的順序返回鍵值。 | Object 的鍵是無(wú)序的 |
Size | Map 的鍵值對(duì)個(gè)數(shù)可以輕易地通過(guò)size 屬性獲取 | Object 的鍵值對(duì)個(gè)數(shù)只能手動(dòng)計(jì)算 |
迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代Object需要以某種方式獲取它的鍵然后才能迭代。 |
性能 | 在頻繁增刪鍵值對(duì)的場(chǎng)景下表現(xiàn)更好。 | 在頻繁添加和刪除鍵值對(duì)的場(chǎng)景下未作出優(yōu)化。 |
(1)Map
map本質(zhì)上就是鍵值對(duì)的集合,但是普通的Object中的鍵值對(duì)中的鍵只能是字符串。而ES6提供的Map數(shù)據(jù)結(jié)構(gòu)類(lèi)似于對(duì)象,但是它的鍵不限制范圍,可以是任意類(lèi)型,是一種更加完善的Hash結(jié)構(gòu)。如果Map的鍵是一個(gè)原始數(shù)據(jù)類(lèi)型,只要兩個(gè)鍵嚴(yán)格相同,就視為是同一個(gè)鍵。
實(shí)際上Map是一個(gè)數(shù)組,它的每一個(gè)數(shù)據(jù)也都是一個(gè)數(shù)組,其形式如下:
const map = [
["name","張三"],
["age",18],
]
Map數(shù)據(jù)結(jié)構(gòu)有以下操作方法:
map.size
? 返回Map結(jié)構(gòu)的成員總數(shù)。Map結(jié)構(gòu)原生提供是三個(gè)遍歷器生成函數(shù)和一個(gè)遍歷方法
const map = new Map([
["foo",1],
["bar",2],
])
for(let key of map.keys()){
console.log(key); // foo bar
}
for(let value of map.values()){
console.log(value); // 1 2
}
for(let items of map.entries()){
console.log(items); // ["foo",1] ["bar",2]
}
map.forEach( (value,key,map) => {
console.log(key,value); // foo 1 bar 2
})
(2)WeakMap
WeakMap 對(duì)象也是一組鍵值對(duì)的集合,其中的鍵是弱引用的。其鍵必須是對(duì)象,原始數(shù)據(jù)類(lèi)型不能作為key值,而值可以是任意的。
該對(duì)象也有以下幾種方法:
其clear()方法已經(jīng)被棄用,所以可以通過(guò)創(chuàng)建一個(gè)空的WeakMap并替換原對(duì)象來(lái)實(shí)現(xiàn)清除。
WeakMap的設(shè)計(jì)目的在于,有時(shí)想在某個(gè)對(duì)象上面存放一些數(shù)據(jù),但是這會(huì)形成對(duì)于這個(gè)對(duì)象的引用。一旦不再需要這兩個(gè)對(duì)象,就必須手動(dòng)刪除這個(gè)引用,否則垃圾回收機(jī)制就不會(huì)釋放對(duì)象占用的內(nèi)存。
而WeakMap的鍵名所引用的對(duì)象都是弱引用,即垃圾回收機(jī)制不將該引用考慮在內(nèi)。因此,只要所引用的對(duì)象的其他引用都被清除,垃圾回收機(jī)制就會(huì)釋放該對(duì)象所占用的內(nèi)存。也就是說(shuō),一旦不再需要,WeakMap 里面的鍵名對(duì)象和所對(duì)應(yīng)的鍵值對(duì)會(huì)自動(dòng)消失,不用手動(dòng)刪除引用。
總結(jié):
全局的對(duì)象( global objects )或稱(chēng)標(biāo)準(zhǔn)內(nèi)置對(duì)象,不要和 "全局對(duì)象(global object)" 混淆。這里說(shuō)的全局的對(duì)象是說(shuō)在
全局作用域里的對(duì)象。全局作用域中的其他對(duì)象可以由用戶的腳本創(chuàng)建或由宿主程序提供。
標(biāo)準(zhǔn)內(nèi)置對(duì)象的分類(lèi):
(1)值屬性,這些全局屬性返回一個(gè)簡(jiǎn)單值,這些值沒(méi)有自己的屬性和方法。
例如 Infinity、NaN、undefined、null 字面量
(2)函數(shù)屬性,全局函數(shù)可以直接調(diào)用,不需要在調(diào)用時(shí)指定所屬對(duì)象,執(zhí)行結(jié)束后會(huì)將結(jié)果直接返回給調(diào)用者。
例如 eval()、parseFloat()、parseInt() 等
(3)基本對(duì)象,基本對(duì)象是定義或使用其他對(duì)象的基礎(chǔ)?;緦?duì)象包括一般對(duì)象、函數(shù)對(duì)象和錯(cuò)誤對(duì)象。
例如 Object、Function、Boolean、Symbol、Error 等
(4)數(shù)字和日期對(duì)象,用來(lái)表示數(shù)字、日期和執(zhí)行數(shù)學(xué)計(jì)算的對(duì)象。
例如 Number、Math、Date
(5)字符串,用來(lái)表示和操作字符串的對(duì)象。
例如 String、RegExp
(6)可索引的集合對(duì)象,這些對(duì)象表示按照索引值來(lái)排序的數(shù)據(jù)集合,包括數(shù)組和類(lèi)型數(shù)組,以及類(lèi)數(shù)組結(jié)構(gòu)的對(duì)象。例如 Array
(7)使用鍵的集合對(duì)象,這些集合對(duì)象在存儲(chǔ)數(shù)據(jù)時(shí)會(huì)使用到鍵,支持按照插入順序來(lái)迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的數(shù)據(jù)會(huì)被組織為一個(gè)數(shù)據(jù)序列。
例如 SIMD 等
(9)結(jié)構(gòu)化數(shù)據(jù),這些對(duì)象用來(lái)表示和操作結(jié)構(gòu)化的緩沖區(qū)數(shù)據(jù),或使用 JSON 編碼的數(shù)據(jù)。
例如 JSON 等
(10)控制抽象對(duì)象
例如 Promise、Generator 等
(11)反射
例如 Reflect、Proxy
(12)國(guó)際化,為了支持多語(yǔ)言處理而加入 ECMAScript 的對(duì)象。
例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他
例如 arguments
總結(jié):
js 中的內(nèi)置對(duì)象主要指的是在程序執(zhí)行前存在全局作用域里的由 js 定義的一些全局值屬性、函數(shù)和用來(lái)實(shí)例化其他對(duì)象的構(gòu)造函數(shù)對(duì)象。一般經(jīng)常用到的如全局變量值 NaN、undefined,全局函數(shù)如 parseInt()、parseFloat() 用來(lái)實(shí)例化對(duì)象的構(gòu)造函數(shù)如 Date、Object 等,還有提供數(shù)學(xué)計(jì)算的單體內(nèi)置對(duì)象如 Math 對(duì)象。
// (1)匹配 16 進(jìn)制顏色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
// (3)匹配 qq 號(hào)
var regex = /^[1-9][0-9]{4,10}$/g;
// (4)手機(jī)號(hào)碼正則
var regex = /^1[34578]\d{9}$/g;
// (5)用戶名正則
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
JSON 是一種基于文本的輕量級(jí)的數(shù)據(jù)交換格式。它可以被任何的編程語(yǔ)言讀取和作為數(shù)據(jù)格式來(lái)傳遞。
在項(xiàng)目開(kāi)發(fā)中,使用 JSON 作為前后端數(shù)據(jù)交換的方式。在前端通過(guò)將一個(gè)符合 JSON 格式的數(shù)據(jù)結(jié)構(gòu)序列化為
JSON 字符串,然后將它傳遞到后端,后端通過(guò) JSON 格式的字符串解析后生成對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),以此來(lái)實(shí)現(xiàn)前后端數(shù)據(jù)的一個(gè)傳遞。
因?yàn)?JSON 的語(yǔ)法是基于 js 的,因此很容易將 JSON 和 js 中的對(duì)象弄混,但是應(yīng)該注意的是 JSON 和 js 中的對(duì)象不是一回事,JSON 中對(duì)象格式更加嚴(yán)格,比如說(shuō)在 JSON 中屬性值不能為函數(shù),不能出現(xiàn) NaN 這樣的屬性值等,因此大多數(shù)的 js 對(duì)象是不符合 JSON 對(duì)象的格式的。
在 js 中提供了兩個(gè)函數(shù)來(lái)實(shí)現(xiàn) js 數(shù)據(jù)結(jié)構(gòu)和 JSON 格式的轉(zhuǎn)換處理,
延遲加載就是等頁(yè)面加載完成之后再加載 JavaScript 文件。 js 延遲加載有助于提高頁(yè)面加載速度。
一般有以下幾種方式:
一個(gè)擁有 length 屬性和若干索引屬性的對(duì)象就可以被稱(chēng)為類(lèi)數(shù)組對(duì)象,類(lèi)數(shù)組對(duì)象和數(shù)組類(lèi)似,但是不能調(diào)用數(shù)組的方法。常見(jiàn)的類(lèi)數(shù)組對(duì)象有 arguments 和 DOM 方法的返回結(jié)果,還有一個(gè)函數(shù)也可以被看作是類(lèi)數(shù)組對(duì)象,因?yàn)樗?length 屬性值,代表可接收的參數(shù)個(gè)數(shù)。
常見(jiàn)的類(lèi)數(shù)組轉(zhuǎn)換為數(shù)組的方法有這樣幾種:
(1)通過(guò) call 調(diào)用數(shù)組的 slice 方法來(lái)實(shí)現(xiàn)轉(zhuǎn)換
Array.prototype.slice.call(arrayLike);
(2)通過(guò) call 調(diào)用數(shù)組的 splice 方法來(lái)實(shí)現(xiàn)轉(zhuǎn)換
Array.prototype.splice.call(arrayLike, 0);
(3)通過(guò) apply 調(diào)用數(shù)組的 concat 方法來(lái)實(shí)現(xiàn)轉(zhuǎn)換
Array.prototype.concat.apply([], arrayLike);
(4)通過(guò) Array.from 方法來(lái)實(shí)現(xiàn)轉(zhuǎn)換
Array.from(arrayLike);
在說(shuō) Unicode
之前需要先了解一下 ASCII
碼:ASCII 碼(American Standard Code for Information Interchange
)稱(chēng)為美國(guó)標(biāo)準(zhǔn)信息交換碼。
ASCII
碼可以表示的編碼有限,要想表示其他語(yǔ)言的編碼,還是要使用 Unicode
來(lái)表示,可以說(shuō) Unicode
是 ASCII
的超集。
Unicode
全稱(chēng) Unicode Translation Format
,又叫做統(tǒng)一碼、萬(wàn)國(guó)碼、單一碼。Unicode
是為了解決傳統(tǒng)的字符編碼方案的局限而產(chǎn)生的,它為每種語(yǔ)言中的每個(gè)字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼,以滿足跨語(yǔ)言、跨平臺(tái)進(jìn)行文本轉(zhuǎn)換、處理的要求。
Unicode
的實(shí)現(xiàn)方式(也就是編碼方式)有很多種,常見(jiàn)的是UTF-8、UTF-16、UTF-32和USC-2。
UTF-8
是使用最廣泛的 Unicode
編碼方式,它是一種可變長(zhǎng)的編碼方式,可以是1—4個(gè)字節(jié)不等,它可以完全兼容 ASCII
碼的128個(gè)字符。
注意: UTF-8
是一種編碼方式,Unicode
是一個(gè)字符集合。
UTF-8
的編碼規(guī)則:
來(lái)看一下具體的 Unicode
編號(hào)范圍與對(duì)應(yīng)的 UTF-8
二進(jìn)制格式 :
編碼范圍(編號(hào)對(duì)應(yīng)的十進(jìn)制數(shù)) | 二進(jìn)制格式 |
---|---|
0x00—0x7F (0-127) | 0xxxxxxx |
0x80—0x7FF (128-2047) | 110xxxxx 10xxxxxx |
0x800—0xFFFF (2048-65535) | 1110xxxx 10xxxxxx 10xxxxxx |
0x10000—0x10FFFF (65536以上) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
那該如何通過(guò)具體的 Unicode
編碼,進(jìn)行具體的 UTF-8
編碼呢?步驟如下:
Unicode
?編碼的所在的編號(hào)范圍,進(jìn)而找到與之對(duì)應(yīng)的二進(jìn)制格式Unicode
?編碼轉(zhuǎn)換為二進(jìn)制數(shù)(去掉最高位的0)X
?中,如果有 ?X
?未填,就設(shè)為0來(lái)看一個(gè)實(shí)際的例子:
“馬” 字的 Unicode
編碼是:0x9A6C
,整數(shù)編號(hào)是 39532
(1)首選確定了該字符在第三個(gè)范圍內(nèi),它的格式是 1110xxxx 10xxxxxx 10xxxxxx
(2)39532對(duì)應(yīng)的二進(jìn)制數(shù)為 1001 1010 0110 1100
(3)將二進(jìn)制數(shù)填入X中,結(jié)果是:11101001 10101001 10101100
1. 平面的概念
在了解 UTF-16
之前,先看一下平面的概念:
Unicode
編碼中有很多很多的字符,它并不是一次性定義的,而是分區(qū)進(jìn)行定義的,每個(gè)區(qū)存放65536(216)個(gè)字符,這稱(chēng)為一個(gè)平面,目前總共有17 個(gè)平面。
最前面的一個(gè)平面稱(chēng)為基本平面,它的碼點(diǎn)從0 — 216-1,寫(xiě)成16進(jìn)制就是 U+0000 — U+FFFF
,那剩下的16個(gè)平面就是輔助平面,碼點(diǎn)范圍是 U+10000—U+10FFFF
。
2. UTF-16 概念:
UTF-16
也是 Unicode
編碼集的一種編碼形式,把 Unicode
字符集的抽象碼位映射為16位長(zhǎng)的整數(shù)(即碼元)的序列,用于數(shù)據(jù)存儲(chǔ)或傳遞。Unicode
字符的碼位需要1個(gè)或者2個(gè)16位長(zhǎng)的碼元來(lái)表示,因此 UTF-16
也是用變長(zhǎng)字節(jié)表示的。
3. UTF-16 編碼規(guī)則:
U+0000—U+FFFF
? 的字符(常用字符集),直接用兩個(gè)字節(jié)表示。U+10000—U+10FFFF
? 之間的字符,需要用四個(gè)字節(jié)表示。4. 編碼識(shí)別
那么問(wèn)題來(lái)了,當(dāng)遇到兩個(gè)字節(jié)時(shí),怎么知道是把它當(dāng)做一個(gè)字符還是和后面的兩個(gè)字節(jié)一起當(dāng)做一個(gè)字符呢?
UTF-16
編碼肯定也考慮到了這個(gè)問(wèn)題,在基本平面內(nèi),從 U+D800 — U+DFFF
是一個(gè)空段,也就是說(shuō)這個(gè)區(qū)間的碼點(diǎn)不對(duì)應(yīng)任何的字符,因此這些空段就可以用來(lái)映射輔助平面的字符。
輔助平面共有 220 個(gè)字符位,因此表示這些字符至少需要 20 個(gè)二進(jìn)制位。UTF-16
將這 20 個(gè)二進(jìn)制位分成兩半,前 10 位映射在 U+D800 — U+DBFF
,稱(chēng)為高位(H),后 10 位映射在 U+DC00 — U+DFFF
,稱(chēng)為低位(L)。這就相當(dāng)于,將一個(gè)輔助平面的字符拆成了兩個(gè)基本平面的字符來(lái)表示。
因此,當(dāng)遇到兩個(gè)字節(jié)時(shí),發(fā)現(xiàn)它的碼點(diǎn)在 U+D800 —U+DBFF
之間,就可以知道,它后面的兩個(gè)字節(jié)的碼點(diǎn)應(yīng)該在 U+DC00 — U+DFFF
之間,這四個(gè)字節(jié)必須放在一起進(jìn)行解讀。
5. 舉例說(shuō)明
以 "" 字為例,它的 Unicode
碼點(diǎn)為 0x21800
,該碼點(diǎn)超出了基本平面的范圍,因此需要用四個(gè)字節(jié)來(lái)表示,步驟如下:
0x21800 - 0x10000
?0001000110 0000000000
?U+D800
? 對(duì)應(yīng)的二進(jìn)制數(shù)為 ?1101100000000000
?, 將 ?0001000110
?填充在它的后10 個(gè)二進(jìn)制位,得到 ?1101100001000110
?,轉(zhuǎn)成 16 進(jìn)制數(shù)為 ?0xD846
?。同理,低位為 ?0xDC00
?,所以這個(gè)字的 ?UTF-16
? 編碼為 ?0xD846 0xDC00
?UTF-32
就是字符所對(duì)應(yīng)編號(hào)的整數(shù)二進(jìn)制形式,每個(gè)字符占四個(gè)字節(jié),這個(gè)是直接進(jìn)行轉(zhuǎn)換的。該編碼方式占用的儲(chǔ)存空間較多,所以使用較少。
比如“馬” 字的Unicode編號(hào)是:U+9A6C
,整數(shù)編號(hào)是 39532
,直接轉(zhuǎn)化為二進(jìn)制:1001 1010 0110 1100
,這就是它的UTF-32編碼。
Unicode、UTF-8、UTF-16、UTF-32有什么區(qū)別?
Unicode
?是編碼字符集(字符集),而 ?UTF-8
?、?UTF-16
?、?UTF-32
?是字符集編碼(編碼規(guī)則);UTF-16
? 使用變長(zhǎng)碼元序列的編碼方式,相較于定長(zhǎng)碼元序列的 ?UTF-32
?算法更復(fù)雜,甚至比同樣是變長(zhǎng)碼元序列的 ?UTF-8
?也更為復(fù)雜,因?yàn)槠湟肓霜?dú)特的代理對(duì)這樣的代理機(jī)制;UTF-8
?需要判斷每個(gè)字節(jié)中的開(kāi)頭標(biāo)志信息,所以如果某個(gè)字節(jié)在傳送過(guò)程中出錯(cuò)了,就會(huì)導(dǎo)致后面的字節(jié)也會(huì)解析出錯(cuò);而 ?UTF-16
?不會(huì)判斷開(kāi)頭標(biāo)志,即使錯(cuò)也只會(huì)錯(cuò)一個(gè)字符,所以容錯(cuò)能力教強(qiáng);UTF-8
?就比 ?UTF-16
?節(jié)省了很多空間;而如果字符內(nèi)容全部是中文這樣類(lèi)似的字符或者混合字符中中文占絕大多數(shù),那么 ?UTF-16
?就占優(yōu)勢(shì)了,可以節(jié)省很多空間;現(xiàn)代計(jì)算機(jī)中數(shù)據(jù)都是以二進(jìn)制的形式存儲(chǔ)的,即0、1兩種狀態(tài),計(jì)算機(jī)對(duì)二進(jìn)制數(shù)據(jù)進(jìn)行的運(yùn)算加減乘除等都是叫位運(yùn)算,即將符號(hào)位共同參與運(yùn)算的運(yùn)算。
常見(jiàn)的位運(yùn)算有以下幾種:
運(yùn)算符 | 描述 | 運(yùn)算規(guī)則 |
---|---|---|
&
|
與 | 兩個(gè)位都為1時(shí),結(jié)果才為1 |
` | ` | 或 |
^
|
異或 | 兩個(gè)位相同為0,相異為1 |
~
|
取反 | 0變1,1變0 |
<<
|
左移 | 各二進(jìn)制位全部左移若干位,高位丟棄,低位補(bǔ)0 |
>>
|
右移 | 各二進(jìn)制位全部右移若干位,正數(shù)左補(bǔ)0,負(fù)數(shù)左補(bǔ)1,右邊丟棄 |
定義: 參加運(yùn)算的兩個(gè)數(shù)據(jù)按二進(jìn)制位進(jìn)行“與”運(yùn)算。
運(yùn)算規(guī)則:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
總結(jié):兩位同時(shí)為1,結(jié)果才為1,否則結(jié)果為0。
例如:3&5 即:
0000 0011
0000 0101
= 0000 0001
因此 3&5 的值為1。
注意:負(fù)數(shù)按補(bǔ)碼形式參加按位與運(yùn)算。
用途:
(1)判斷奇偶
只要根據(jù)最未位是0還是1來(lái)決定,為0就是偶數(shù),為1就是奇數(shù)。因此可以用 if ((i & 1) == 0)
代替 if (i % 2 == 0)
來(lái)判斷a是不是偶數(shù)。
(2)清零
如果想將一個(gè)單元清零,即使其全部二進(jìn)制位為0,只要與一個(gè)各位都為零的數(shù)值相與,結(jié)果為零。
定義: 參加運(yùn)算的兩個(gè)對(duì)象按二進(jìn)制位進(jìn)行“或”運(yùn)算。
運(yùn)算規(guī)則:
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
總結(jié):參加運(yùn)算的兩個(gè)對(duì)象只要有一個(gè)為1,其值為1。
例如:3|5即:
0000 0011
0000 0101
= 0000 0111
因此,3|5的值為7。
注意:負(fù)數(shù)按補(bǔ)碼形式參加按位或運(yùn)算。
定義: 參加運(yùn)算的兩個(gè)數(shù)據(jù)按二進(jìn)制位進(jìn)行“異或”運(yùn)算。
運(yùn)算規(guī)則:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
總結(jié):參加運(yùn)算的兩個(gè)對(duì)象,如果兩個(gè)相應(yīng)位相同為0,相異為1。
例如:3|5即:
0000 0011
0000 0101
= 0000 0110
因此,3^5的值為6。
異或運(yùn)算的性質(zhì):
(a^b)^c == a^(b^c)
?(a + b)^c == a^b + b^c
?x^x=0,x^0=x
?a^b^b=a^0=a;
?定義: 參加運(yùn)算的一個(gè)數(shù)據(jù)按二進(jìn)制進(jìn)行“取反”運(yùn)算。
運(yùn)算規(guī)則:
~ 1 = 0
~ 0 = 1
總結(jié):對(duì)一個(gè)二進(jìn)制數(shù)按位取反,即將0變1,1變0。
例如:~6 即:
0000 0110
= 1111 1001
在計(jì)算機(jī)中,正數(shù)用原碼表示,負(fù)數(shù)使用補(bǔ)碼存儲(chǔ),首先看最高位,最高位1表示負(fù)數(shù),0表示正數(shù)。此計(jì)算機(jī)二進(jìn)制碼為負(fù)數(shù),最高位為符號(hào)位。
當(dāng)發(fā)現(xiàn)按位取反為負(fù)數(shù)時(shí),就直接取其補(bǔ)碼,變?yōu)槭M(jìn)制:
0000 0110
= 1111 1001
反碼:1000 0110
補(bǔ)碼:1000 0111
因此,~6的值為-7。
定義: 將一個(gè)運(yùn)算對(duì)象的各二進(jìn)制位全部左移若干位,左邊的二進(jìn)制位丟棄,右邊補(bǔ)0。
設(shè) a=1010 1110,a = a<< 2 將a的二進(jìn)制位左移2位、右補(bǔ)0,即得a=1011 1000。
若左移時(shí)舍棄的高位不包含1,則每左移一位,相當(dāng)于該數(shù)乘以2。
定義: 將一個(gè)數(shù)的各二進(jìn)制位全部右移若干位,正數(shù)左補(bǔ)0,負(fù)數(shù)左補(bǔ)1,右邊丟棄。
例如:a=a>>2 將a的二進(jìn)制位右移2位,左補(bǔ)0 或者 左補(bǔ)1得看被移數(shù)是正還是負(fù)。
操作數(shù)每右移一位,相當(dāng)于該數(shù)除以2。
上面提到了補(bǔ)碼、反碼等知識(shí),這里就補(bǔ)充一下。
計(jì)算機(jī)中的有符號(hào)數(shù)有三種表示方法,即原碼、反碼和補(bǔ)碼。三種表示方法均有符號(hào)位和數(shù)值位兩部分,符號(hào)位都是用0表示“正”,用1表示“負(fù)”,而數(shù)值位,三種表示方法各不相同。
(1)原碼
原碼就是一個(gè)數(shù)的二進(jìn)制數(shù)。
例如:10的原碼為0000 1010
(2)反碼
例如:-10
原碼:1000 1010
反碼:1111 0101
(3)補(bǔ)碼
例如:-10
原碼:1000 1010
反碼:1111 0101
補(bǔ)碼:1111 0110
arguments
是一個(gè)對(duì)象,它的屬性是從 0 開(kāi)始依次遞增的數(shù)字,還有 callee
和 length
等屬性,與數(shù)組相似;但是它卻沒(méi)有數(shù)組常見(jiàn)的方法屬性,如 forEach
, reduce
等,所以叫它們類(lèi)數(shù)組。
要遍歷類(lèi)數(shù)組,有三個(gè)方法:
(1)將數(shù)組的方法應(yīng)用到類(lèi)數(shù)組上,這時(shí)候就可以使用 call
和 apply
方法,如:
function foo(){
Array.prototype.forEach.call(arguments, a => console.log(a))
}
(2)使用Array.from方法將類(lèi)數(shù)組轉(zhuǎn)化成數(shù)組:?
function foo(){
const arrArgs = Array.from(arguments)
arrArgs.forEach(a => console.log(a))
}
(3)使用展開(kāi)運(yùn)算符將類(lèi)數(shù)組轉(zhuǎn)化成數(shù)組
function foo(){
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a))
}
一個(gè)擁有 length 屬性和若干索引屬性的對(duì)象就可以被稱(chēng)為類(lèi)數(shù)組對(duì)象,類(lèi)數(shù)組對(duì)象和數(shù)組類(lèi)似,但是不能調(diào)用數(shù)組的方法。常見(jiàn)的類(lèi)數(shù)組對(duì)象有 arguments 和 DOM 方法的返回結(jié)果,函數(shù)參數(shù)也可以被看作是類(lèi)數(shù)組對(duì)象,因?yàn)樗?length屬性值,代表可接收的參數(shù)個(gè)數(shù)。
常見(jiàn)的類(lèi)數(shù)組轉(zhuǎn)換為數(shù)組的方法有這樣幾種:
Array.prototype.slice.call(arrayLike);
Array.prototype.splice.call(arrayLike, 0);
Array.prototype.concat.apply([], arrayLike);
Array.from(arrayLike);
AJAX是 Asynchronous JavaScript and XML 的縮寫(xiě),指的是通過(guò) JavaScript 的 異步通信,從服務(wù)器獲取 XML 文檔從中提取數(shù)據(jù),再更新當(dāng)前網(wǎng)頁(yè)的對(duì)應(yīng)部分,而不用刷新整個(gè)網(wǎng)頁(yè)。
創(chuàng)建AJAX請(qǐng)求的步驟:
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 創(chuàng)建 Http 請(qǐng)求
xhr.open("GET", url, true);
// 設(shè)置狀態(tài)監(jiān)聽(tīng)函數(shù)
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 當(dāng)請(qǐng)求成功時(shí)
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 設(shè)置請(qǐng)求失敗時(shí)的監(jiān)聽(tīng)函數(shù)
xhr.onerror = function() {
console.error(this.statusText);
};
// 設(shè)置請(qǐng)求頭信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 發(fā)送 Http 請(qǐng)求
xhr.send(null);
使用Promise封裝AJAX:
// promise 封裝實(shí)現(xiàn):
function getJSON(url) {
// 創(chuàng)建一個(gè) promise 對(duì)象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一個(gè) http 請(qǐng)求
xhr.open("GET", url, true);
// 設(shè)置狀態(tài)的監(jiān)聽(tīng)函數(shù)
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 當(dāng)請(qǐng)求成功或失敗時(shí),改變 promise 的狀態(tài)
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 設(shè)置錯(cuò)誤監(jiān)聽(tīng)函數(shù)
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 設(shè)置響應(yīng)的數(shù)據(jù)類(lèi)型
xhr.responseType = "json";
// 設(shè)置請(qǐng)求頭信息
xhr.setRequestHeader("Accept", "application/json");
// 發(fā)送 http 請(qǐng)求
xhr.send(null);
});
return promise;
}
變量提升的表現(xiàn)是,無(wú)論在函數(shù)中何處位置聲明的變量,好像都被提升到了函數(shù)的首部,可以在變量聲明前訪問(wèn)到而不會(huì)報(bào)錯(cuò)。
造成變量聲明提升的本質(zhì)原因是 js 引擎在代碼執(zhí)行前有一個(gè)解析的過(guò)程,創(chuàng)建了執(zhí)行上下文,初始化了一些代碼執(zhí)行時(shí)需要用到的對(duì)象。當(dāng)訪問(wèn)一個(gè)變量時(shí),會(huì)到當(dāng)前執(zhí)行上下文中的作用域鏈中去查找,而作用域鏈的首端指向的是當(dāng)前執(zhí)行上下文的變量對(duì)象,這個(gè)變量對(duì)象是執(zhí)行上下文的一個(gè)屬性,它包含了函數(shù)的形參、所有的函數(shù)和變量聲明,這個(gè)對(duì)象的是在代碼解析的時(shí)候創(chuàng)建的。
首先要知道,JS在拿到一個(gè)變量或者一個(gè)函數(shù)的時(shí)候,會(huì)有兩步操作,即解析和執(zhí)行。
那為什么會(huì)進(jìn)行變量提升呢?主要有以下兩個(gè)原因:
(1)提高性能
在JS代碼執(zhí)行之前,會(huì)進(jìn)行語(yǔ)法檢查和預(yù)編譯,并且這一操作只進(jìn)行一次。這么做就是為了提高性能,如果沒(méi)有這一步,那么每次執(zhí)行代碼前都必須重新解析一遍該變量(函數(shù)),而這是沒(méi)有必要的,因?yàn)樽兞浚ê瘮?shù))的代碼并不會(huì)改變,解析一遍就夠了。
在解析的過(guò)程中,還會(huì)為函數(shù)生成預(yù)編譯代碼。在預(yù)編譯時(shí),會(huì)統(tǒng)計(jì)聲明了哪些變量、創(chuàng)建了哪些函數(shù),并對(duì)函數(shù)的代碼進(jìn)行壓縮,去除注釋、不必要的空白等。這樣做的好處就是每次執(zhí)行函數(shù)時(shí)都可以直接為該函數(shù)分配棧空間(不需要再解析一遍去獲取代碼中聲明了哪些變量,創(chuàng)建了哪些函數(shù)),并且因?yàn)榇a壓縮的原因,代碼執(zhí)行也更快了。
(2)容錯(cuò)性更好
變量提升可以在一定程度上提高JS的容錯(cuò)性,看下面的代碼:
a = 1;
var a;
console.log(a);
如果沒(méi)有變量提升,這兩行代碼就會(huì)報(bào)錯(cuò),但是因?yàn)橛辛俗兞刻嵘?,這段代碼就可以正常執(zhí)行。
雖然,在可以開(kāi)發(fā)過(guò)程中,可以完全避免這樣寫(xiě),但是有時(shí)代碼很復(fù)雜的時(shí)候。可能因?yàn)槭韬龆仁褂煤蠖x了,這樣也不會(huì)影響正常使用。由于變量提升的存在,而會(huì)正常運(yùn)行。
總結(jié):
變量提升雖然有一些優(yōu)點(diǎn),但是他也會(huì)造成一定的問(wèn)題,在ES6中提出了let、const來(lái)定義變量,它們就沒(méi)有變量提升的機(jī)制。下面看一下變量提升可能會(huì)導(dǎo)致的問(wèn)題:
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
}
fn(); // undefined
在這個(gè)函數(shù)中,原本是要打印出外層的tmp變量,但是因?yàn)樽兞刻嵘膯?wèn)題,內(nèi)層定義的tmp被提到函數(shù)內(nèi)部的最頂部,相當(dāng)于覆蓋了外層的tmp,所以打印結(jié)果為undefined。
var tmp = 'hello world';
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // 11
由于遍歷時(shí)定義的i會(huì)變量提升成為一個(gè)全局變量,在函數(shù)結(jié)束之后不會(huì)被銷(xiāo)毀,所以打印出來(lái)11。
尾調(diào)用指的是函數(shù)的最后一步調(diào)用另一個(gè)函數(shù)。代碼執(zhí)行是基于執(zhí)行棧的,所以當(dāng)在一個(gè)函數(shù)里調(diào)用另一個(gè)函數(shù)時(shí),會(huì)保留當(dāng)前的執(zhí)行上下文,然后再新建另外一個(gè)執(zhí)行上下文加入棧中。使用尾調(diào)用的話,因?yàn)橐呀?jīng)是函數(shù)的最后一步,所以這時(shí)可以不必再保留當(dāng)前的執(zhí)行上下文,從而節(jié)省了內(nèi)存,這就是尾調(diào)用優(yōu)化。但是 ES6 的尾調(diào)用優(yōu)化只在嚴(yán)格模式下開(kāi)啟,正常模式是無(wú)效的。
ES6 Module和CommonJS模塊的區(qū)別:
ES6 Module和CommonJS模塊的共同點(diǎn):
CommonJs require后執(zhí)行整個(gè)模塊代碼 且加載同一個(gè)模塊只會(huì)執(zhí)行一次,執(zhí)行完進(jìn)行緩存 ES6 Module 像const一樣指定 import {xx} from 'xxx' 無(wú)法給xx賦值 但可以改變xx的屬性和方法
CommonJS
1.對(duì)于基本數(shù)據(jù)類(lèi)型,屬于復(fù)制。即會(huì)被模塊緩存。同時(shí),在另一個(gè)模塊可以對(duì)該模塊輸出的變量重新賦值。
2.對(duì)于復(fù)雜數(shù)據(jù)類(lèi)型,屬于淺拷貝。由于兩個(gè)模塊引用的對(duì)象指向同一個(gè)內(nèi)存空間,因此對(duì)該模塊的值做修改時(shí)會(huì)影響另一個(gè)模塊。
3.當(dāng)使用require命令加載某個(gè)模塊時(shí),就會(huì)運(yùn)行整個(gè)模塊的代碼。
4.當(dāng)使用require命令加載同一個(gè)模塊時(shí),不會(huì)再執(zhí)行該模塊,而是取到緩存之中的值。也就是說(shuō),CommonJS模塊無(wú)論加載多少次,都只會(huì)在第一次加載時(shí)運(yùn)行一次,以后再加載,就返回第一次運(yùn)行的結(jié)果,除非手動(dòng)清除系統(tǒng)緩存。
5.循環(huán)加載時(shí),屬于加載時(shí)執(zhí)行。即腳本代碼在require的時(shí)候,就會(huì)全部執(zhí)行。一旦出現(xiàn)某個(gè)模塊被"循環(huán)加載",就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會(huì)輸出。
ES6模塊
1.ES6模塊中的值屬于【動(dòng)態(tài)只讀引用】
2.對(duì)于只讀來(lái)說(shuō),即不允許修改引入變量的值,import的變量是只讀的,不論是基本數(shù)據(jù)類(lèi)型還是復(fù)雜數(shù)據(jù)類(lèi)型。當(dāng)模塊遇到import命令時(shí),就會(huì)生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí),再根據(jù)這個(gè)只讀引用,到被加載的那個(gè)模塊里面去取值。
3.對(duì)于動(dòng)態(tài)來(lái)說(shuō),原始值發(fā)生變化,import加載的值也會(huì)發(fā)生變化。不論是基本數(shù)據(jù)類(lèi)型還是復(fù)雜數(shù)據(jù)類(lèi)型。
4.循環(huán)加載時(shí),ES6模塊是動(dòng)態(tài)引用。只要兩個(gè)模塊之間存在某個(gè)引用,代碼就能夠執(zhí)行。
DOM 節(jié)點(diǎn)的獲取的API及使用:
getElementById // 按照 id 查詢
getElementsByTagName // 按照標(biāo)簽名查詢
getElementsByClassName // 按照類(lèi)名查詢
querySelectorAll // 按照 css 選擇器查詢
// 按照 id 查詢
var imooc = document.getElementById('imooc') // 查詢到 id 為 imooc 的元素
// 按照標(biāo)簽名查詢
var pList = document.getElementsByTagName('p') // 查詢到標(biāo)簽為 p 的集合
console.log(divList.length)
console.log(divList[0])
// 按照類(lèi)名查詢
var moocList = document.getElementsByClassName('mooc') // 查詢到類(lèi)名為 mooc 的集合
// 按照 css 選擇器查詢
var pList = document.querySelectorAll('.mooc') // 查詢到類(lèi)名為 mooc 的集合
創(chuàng)建一個(gè)新節(jié)點(diǎn),并把它添加到指定節(jié)點(diǎn)的后面。已知的 HTML 結(jié)構(gòu)如下:
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是標(biāo)題</h1>
</div>
</body>
</html>
要求添加一個(gè)有內(nèi)容的 span 節(jié)點(diǎn)到 id 為 title 的節(jié)點(diǎn)后面,做法就是:
// 首先獲取父節(jié)點(diǎn)
var container = document.getElementById('container')
// 創(chuàng)建新節(jié)點(diǎn)
var targetSpan = document.createElement('span')
// 設(shè)置 span 節(jié)點(diǎn)的內(nèi)容
targetSpan.innerHTML = 'hello world'
// 把新創(chuàng)建的元素塞進(jìn)父節(jié)點(diǎn)里去
container.appendChild(targetSpan)
刪除指定的 DOM 節(jié)點(diǎn),已知的 HTML 結(jié)構(gòu)如下:
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是標(biāo)題</h1>
</div>
</body>
</html>
需要?jiǎng)h除 id 為 title 的元素,做法是:
// 獲取目標(biāo)元素的父元素
var container = document.getElementById('container')
// 獲取目標(biāo)元素
var targetNode = document.getElementById('title')
// 刪除目標(biāo)元素
container.removeChild(targetNode)
或者通過(guò)子節(jié)點(diǎn)數(shù)組來(lái)完成刪除:
// 獲取目標(biāo)元素的父元素
var container = document.getElementById('container')
// 獲取目標(biāo)元素
var targetNode = container.childNodes[1]
// 刪除目標(biāo)元素
container.removeChild(targetNode)
修改 DOM 元素這個(gè)動(dòng)作可以分很多維度,比如說(shuō)移動(dòng) DOM 元素的位置,修改 DOM 元素的屬性等。
將指定的兩個(gè) DOM 元素交換位置,已知的 HTML 結(jié)構(gòu)如下:
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是標(biāo)題</h1>
<p id="content">我是內(nèi)容</p>
</div>
</body>
</html>
現(xiàn)在需要調(diào)換 title 和 content 的位置,可以考慮 insertBefore 或者 appendChild:
// 獲取父元素
var container = document.getElementById('container')
// 獲取兩個(gè)需要被交換的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交換兩個(gè)元素,把 content 置于 title 前面
container.insertBefore(content, title)
use strict 是一種 ECMAscript5 添加的(嚴(yán)格模式)運(yùn)行模式,這種模式使得 Javascript 在更嚴(yán)格的條件下運(yùn)行。設(shè)立嚴(yán)格模式的目的如下:
區(qū)別:
兩者對(duì)比:強(qiáng)類(lèi)型語(yǔ)言在速度上可能略遜色于弱類(lèi)型語(yǔ)言,但是強(qiáng)類(lèi)型語(yǔ)言帶來(lái)的嚴(yán)謹(jǐn)性可以有效地幫助避免許多錯(cuò)誤。
(1)解釋型語(yǔ)言
使用專(zhuān)門(mén)的解釋器對(duì)源程序逐行解釋成特定平臺(tái)的機(jī)器碼并立即執(zhí)行。是代碼在執(zhí)行時(shí)才被解釋器一行行動(dòng)態(tài)翻譯和執(zhí)行,而不是在執(zhí)行之前就完成翻譯。解釋型語(yǔ)言不需要事先編譯,其直接將源代碼解釋成機(jī)器碼并立即執(zhí)行,所以只要某一平臺(tái)提供了相應(yīng)的解釋器即可運(yùn)行該程序。其特點(diǎn)總結(jié)如下
(2)編譯型語(yǔ)言
使用專(zhuān)門(mén)的編譯器,針對(duì)特定的平臺(tái),將高級(jí)語(yǔ)言源代碼一次性的編譯成可被該平臺(tái)硬件執(zhí)行的機(jī)器碼,并包裝成該平臺(tái)所能識(shí)別的可執(zhí)行性程序的格式。在編譯型語(yǔ)言寫(xiě)的程序執(zhí)行之前,需要一個(gè)專(zhuān)門(mén)的編譯過(guò)程,把源代碼編譯成機(jī)器語(yǔ)言的文件,如exe格式的文件,以后要再運(yùn)行時(shí),直接使用編譯結(jié)果即可,如直接運(yùn)行exe文件。因?yàn)橹恍杈幾g一次,以后運(yùn)行時(shí)不需要編譯,所以編譯型語(yǔ)言執(zhí)行效率高。其特點(diǎn)總結(jié)如下:
兩者主要區(qū)別在于:前者源程序編譯后即可在該平臺(tái)運(yùn)行,后者是在運(yùn)行期間才編譯。所以前者運(yùn)行速度快,后者跨平臺(tái)性好。
for…of 是ES6新增的遍歷方式,允許遍歷一個(gè)含有iterator接口的數(shù)據(jù)結(jié)構(gòu)(數(shù)組、對(duì)象等)并且返回各項(xiàng)的值,和ES3中的for…in的區(qū)別如下
總結(jié):for...in 循環(huán)主要是為了遍歷對(duì)象而生,不適用于遍歷數(shù)組;for...of 循環(huán)可以用來(lái)遍歷數(shù)組、類(lèi)數(shù)組對(duì)象,字符串、Set、Map 以及 Generator 對(duì)象。
for…of是作為ES6新增的遍歷方式,允許遍歷一個(gè)含有iterator接口的數(shù)據(jù)結(jié)構(gòu)(數(shù)組、對(duì)象等)并且返回各項(xiàng)的值,普通的對(duì)象用for..of遍歷是會(huì)報(bào)錯(cuò)的。
如果需要遍歷的對(duì)象是類(lèi)數(shù)組對(duì)象,用Array.from轉(zhuǎn)成數(shù)組即可。
var obj = {
0:'one',
1:'two',
length: 2
};
obj = Array.from(obj);
for(var k of obj){
console.log(k)
}
如果不是類(lèi)數(shù)組對(duì)象,就給對(duì)象添加一個(gè)[Symbol.iterator]屬性,并指向一個(gè)迭代器即可。
//方法一:
var obj = {
a:1,
b:2,
c:3
};
obj[Symbol.iterator] = function(){
var keys = Object.keys(this);
var count = 0;
return {
next(){
if(count<keys.length){
return {value: obj[keys[count++]],done:false};
}else{
return {value:undefined,done:true};
}
}
}
};
for(var k of obj){
console.log(k);
}
// 方法二
var obj = {
a:1,
b:2,
c:3
};
obj[Symbol.iterator] = function*(){
var keys = Object.keys(obj);
for(var k of keys){
yield [k,obj[k]]
}
};
for(var [k,v] of obj){
console.log(k,v);
}
(1)AJAX
Ajax 即“AsynchronousJavascriptAndXML”(異步 JavaScript 和 XML),是指一種創(chuàng)建交互式網(wǎng)頁(yè)應(yīng)用的網(wǎng)頁(yè)開(kāi)發(fā)技術(shù)。它是一種在無(wú)需重新加載整個(gè)網(wǎng)頁(yè)的情況下,能夠更新部分網(wǎng)頁(yè)的技術(shù)。通過(guò)在后臺(tái)與服務(wù)器進(jìn)行少量數(shù)據(jù)交換,Ajax 可以使網(wǎng)頁(yè)實(shí)現(xiàn)異步更新。這意味著可以在不重新加載整個(gè)網(wǎng)頁(yè)的情況下,對(duì)網(wǎng)頁(yè)的某部分進(jìn)行更新。傳統(tǒng)的網(wǎng)頁(yè)(不使用 Ajax)如果需要更新內(nèi)容,必須重載整個(gè)網(wǎng)頁(yè)頁(yè)面。其缺點(diǎn)如下:
(2)Fetch
fetch號(hào)稱(chēng)是AJAX的替代品,是在ES6出現(xiàn)的,使用了ES6中的promise對(duì)象。Fetch是基于promise設(shè)計(jì)的。Fetch的代碼結(jié)構(gòu)比起ajax簡(jiǎn)單多。fetch不是ajax的進(jìn)一步封裝,而是原生js,沒(méi)有使用XMLHttpRequest對(duì)象。
fetch的優(yōu)點(diǎn):
fetch的缺點(diǎn):
(3)Axios
Axios 是一種基于Promise封裝的HTTP客戶端,其特點(diǎn)如下:
方法 | 是否改變?cè)瓟?shù)組 | 特點(diǎn) |
---|---|---|
forEach() | 否 | 數(shù)組方法,不改變?cè)瓟?shù)組,沒(méi)有返回值 |
map() | 否 | 數(shù)組方法,不改變?cè)瓟?shù)組,有返回值,可鏈?zhǔn)秸{(diào)用 |
filter() | 否 | 數(shù)組方法,過(guò)濾數(shù)組,返回包含符合條件的元素的數(shù)組,可鏈?zhǔn)秸{(diào)用 |
for...of | 否 | for...of遍歷具有Iterator迭代器的對(duì)象的屬性,返回的是數(shù)組的元素、對(duì)象的屬性值,不能遍歷普通的obj對(duì)象,將異步循環(huán)變成同步循環(huán) |
every() 和 some() | 否 | 數(shù)組方法,some()只要有一個(gè)是true,便返回true;而every()只要有一個(gè)是false,便返回false. |
find() 和 findIndex() | 否 | 數(shù)組方法,find()返回的是第一個(gè)符合條件的值;findIndex()返回的是第一個(gè)返回條件的值的索引值 |
reduce() 和 reduceRight() | 否 | 數(shù)組方法,reduce()對(duì)數(shù)組正序操作;reduceRight()對(duì)數(shù)組逆序操作 |
這方法都是用來(lái)遍歷數(shù)組的,兩者區(qū)別如下:
在JavaScript中是使用構(gòu)造函數(shù)來(lái)新建一個(gè)對(duì)象的,每一個(gè)構(gòu)造函數(shù)的內(nèi)部都有一個(gè) prototype 屬性,它的屬性值是一個(gè)對(duì)象,這個(gè)對(duì)象包含了可以由該構(gòu)造函數(shù)的所有實(shí)例共享的屬性和方法。當(dāng)使用構(gòu)造函數(shù)新建一個(gè)對(duì)象后,在這個(gè)對(duì)象的內(nèi)部將包含一個(gè)指針,這個(gè)指針指向構(gòu)造函數(shù)的 prototype 屬性對(duì)應(yīng)的值,在 ES5 中這個(gè)指針被稱(chēng)為對(duì)象的原型。一般來(lái)說(shuō)不應(yīng)該能夠獲取到這個(gè)值的,但是現(xiàn)在瀏覽器中都實(shí)現(xiàn)了 proto 屬性來(lái)訪問(wèn)這個(gè)屬性,但是最好不要使用這個(gè)屬性,因?yàn)樗皇且?guī)范中規(guī)定的。ES5
中新增了一個(gè) Object.getPrototypeOf() 方法,可以通過(guò)這個(gè)方法來(lái)獲取對(duì)象的原型。
當(dāng)訪問(wèn)一個(gè)對(duì)象的屬性時(shí),如果這個(gè)對(duì)象內(nèi)部不存在這個(gè)屬性,那么它就會(huì)去它的原型對(duì)象里找這個(gè)屬性,這個(gè)原型對(duì)象又會(huì)有自己的原型,于是就這樣一直找下去,也就是原型鏈的概念。原型鏈的盡頭一般來(lái)說(shuō)都是 Object.prototype 所以這就是新建的對(duì)象為什么能夠使用 toString() 等方法的原因。
特點(diǎn):JavaScript 對(duì)象是通過(guò)引用來(lái)傳遞的,創(chuàng)建的每個(gè)新對(duì)象實(shí)體中并沒(méi)有一份屬于自己的原型副本。當(dāng)修改原型時(shí),與之相關(guān)的對(duì)象也會(huì)繼承這一改變。
function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重寫(xiě)原型
Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false
可以看到重寫(xiě)原型的時(shí)候p的構(gòu)造函數(shù)不是指向Person了,因?yàn)橹苯咏oPerson的原型對(duì)象直接用對(duì)象賦值時(shí),它的構(gòu)造函數(shù)指向的了根構(gòu)造函數(shù)Object,所以這時(shí)候 p.constructor === Object
,而不是 p.constructor === Person
。要想成立,就要用constructor指回來(lái):
Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
p.__proto__ // Person.prototype
Person.prototype.__proto__ // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor // Person
由于 Object
是構(gòu)造函數(shù),原型鏈終點(diǎn)是 Object.prototype.__proto__
,而 Object.prototype.__proto__=== null // true
,所以,原型鏈的終點(diǎn)是 null
。原型鏈上的所有原型都是對(duì)象,所有的對(duì)象最終都是由 Object
構(gòu)造的,而 Object.prototype
的下一級(jí)是 Object.prototype.__proto__
。
使用后 hasOwnProperty()
方法來(lái)判斷屬性是否屬于原型鏈的屬性:
function iterate(obj){
var res=[];
for(var key in obj){
if(obj.hasOwnProperty(key))
res.push(key+': '+obj[key]);
}
return res;
}
閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中變量的函數(shù),創(chuàng)建閉包的最常見(jiàn)的方式就是在一個(gè)函數(shù)內(nèi)創(chuàng)建另一個(gè)函數(shù),創(chuàng)建的函數(shù)可以訪問(wèn)到當(dāng)前函數(shù)的局部變量。
閉包有兩個(gè)常用的用途;
比如,函數(shù) A 內(nèi)部有一個(gè)函數(shù) B,函數(shù) B 可以訪問(wèn)到函數(shù) A 中的變量,那么函數(shù) B 就是閉包。
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1
在 JS 中,閉包存在的意義就是讓我們可以間接訪問(wèn)函數(shù)內(nèi)部的變量。經(jīng)典面試題:循環(huán)中使用閉包解決 var 定義函數(shù)的問(wèn)題
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
首先因?yàn)?nbsp;setTimeout
是個(gè)異步函數(shù),所以會(huì)先把循環(huán)全部執(zhí)行完畢,這時(shí)候 i
就是 6 了,所以會(huì)輸出一堆 6。解決辦法有三種:
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
在上述代碼中,首先使用了立即執(zhí)行函數(shù)將 i
傳入函數(shù)內(nèi)部,這個(gè)時(shí)候值就被固定在了參數(shù) j
上面不會(huì)改變,當(dāng)下次執(zhí)行 timer
這個(gè)閉包的時(shí)候,就可以使用外部函數(shù)的變量 j
,從而達(dá)到目的。
setTimeout
?的第三個(gè)參數(shù),這個(gè)參數(shù)會(huì)被當(dāng)成 ?timer
?函數(shù)的參數(shù)傳入。for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i * 1000,
i
)
}
let
?定義 ?i
? 了來(lái)解決問(wèn)題了,這個(gè)也是最為推薦的方式for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
(1)全局作用域
(2)函數(shù)作用域
{ }
?包裹的代碼片段)作用域鏈:
在當(dāng)前作用域中查找所需變量,但是該作用域沒(méi)有這個(gè)變量,那這個(gè)變量就是自由變量。如果在自己作用域找不到該變量就去父級(jí)作用域查找,依次向上級(jí)作用域查找,直到訪問(wèn)到window對(duì)象就被終止,這一層層的關(guān)系就是作用域鏈。
作用域鏈的作用是保證對(duì)執(zhí)行環(huán)境有權(quán)訪問(wèn)的所有變量和函數(shù)的有序訪問(wèn),通過(guò)作用域鏈,可以訪問(wèn)到外層環(huán)境的變量和函數(shù)。
作用域鏈的本質(zhì)上是一個(gè)指向變量對(duì)象的指針列表。變量對(duì)象是一個(gè)包含了執(zhí)行環(huán)境中所有變量和函數(shù)的對(duì)象。作用域鏈的前端始終都是當(dāng)前執(zhí)行上下文的變量對(duì)象。全局執(zhí)行上下文的變量對(duì)象(也就是全局對(duì)象)始終是作用域鏈的最后一個(gè)對(duì)象。
當(dāng)查找一個(gè)變量時(shí),如果當(dāng)前執(zhí)行環(huán)境中沒(méi)有找到,可以沿著作用域鏈向后查找。
(1)全局執(zhí)行上下文
任何不在函數(shù)內(nèi)部的都是全局執(zhí)行上下文,它首先會(huì)創(chuàng)建一個(gè)全局的window對(duì)象,并且設(shè)置this的值等于這個(gè)全局對(duì)象,一個(gè)程序中只有一個(gè)全局執(zhí)行上下文。
(2)函數(shù)執(zhí)行上下文
當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),就會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文,函數(shù)的上下文可以有任意多個(gè)。
(3)eval
函數(shù)執(zhí)行上下文
執(zhí)行在eval函數(shù)中的代碼會(huì)有屬于他自己的執(zhí)行上下文,不過(guò)eval函數(shù)不常使用,不做介紹。
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
//執(zhí)行順序
//先執(zhí)行second(),在執(zhí)行first()
創(chuàng)建執(zhí)行上下文有兩個(gè)階段:創(chuàng)建階段和執(zhí)行階段
1)創(chuàng)建階段
(1)this綁定
(2)創(chuàng)建詞法環(huán)境組件
(3)創(chuàng)建變量環(huán)境組件
2)執(zhí)行階段
此階段會(huì)完成對(duì)變量的分配,最后執(zhí)行完代碼。
簡(jiǎn)單來(lái)說(shuō)執(zhí)行上下文就是指:
在執(zhí)行一點(diǎn)JS代碼之前,需要先解析代碼。解析的時(shí)候會(huì)先創(chuàng)建一個(gè)全局執(zhí)行上下文環(huán)境,先把代碼中即將執(zhí)行的變量、函數(shù)聲明都拿出來(lái),變量先賦值為undefined,函數(shù)先聲明好可使用。這一步執(zhí)行完了,才開(kāi)始正式的執(zhí)行程序。
在一個(gè)函數(shù)執(zhí)行之前,也會(huì)創(chuàng)建一個(gè)函數(shù)執(zhí)行上下文環(huán)境,跟全局執(zhí)行上下文類(lèi)似,不過(guò)函數(shù)執(zhí)行上下文會(huì)多出this、arguments和函數(shù)的參數(shù)。
this
?,?arguments
?this 是執(zhí)行上下文中的一個(gè)屬性,它指向最后一次調(diào)用這個(gè)方法的對(duì)象。在實(shí)際開(kāi)發(fā)中,this 的指向可以通過(guò)四種調(diào)用模式來(lái)判斷。
這四種方式,使用構(gòu)造器調(diào)用模式的優(yōu)先級(jí)最高,然后是 apply、call 和 bind 調(diào)用模式,然后是方法調(diào)用模式,然后是函數(shù)調(diào)用模式。
它們的作用一模一樣,區(qū)別僅在于傳入?yún)?shù)的形式的不同。
(1)call 函數(shù)的實(shí)現(xiàn)步驟:
Function.prototype.myCall = function(context) {
// 判斷調(diào)用對(duì)象
if (typeof this !== "function") {
console.error("type error");
}
// 獲取參數(shù)
let args = [...arguments].slice(1),
result = null;
// 判斷 context 是否傳入,如果未傳入則設(shè)置為 window
context = context || window;
// 將調(diào)用函數(shù)設(shè)為對(duì)象的方法
context.fn = this;
// 調(diào)用函數(shù)
result = context.fn(...args);
// 將屬性刪除
delete context.fn;
return result;
};
(2)apply 函數(shù)的實(shí)現(xiàn)步驟:
Function.prototype.myApply = function(context) {
// 判斷調(diào)用對(duì)象是否為函數(shù)
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判斷 context 是否存在,如果未傳入則為 window
context = context || window;
// 將函數(shù)設(shè)為對(duì)象的方法
context.fn = this;
// 調(diào)用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 將屬性刪除
delete context.fn;
return result;
};
(3)bind 函數(shù)的實(shí)現(xiàn)步驟:
Function.prototype.myBind = function(context) {
// 判斷調(diào)用對(duì)象是否為函數(shù)
if(typeof this !== 'function'){
console.error('err')
}
let args = [...arguments].slice(1)
let fn = this
const func = function(){
return fn.apply(this instanceof fn ? this:context, args.concat([...arguments]))
}
func.prototype = Object.create(fn.prototype)
func.prototype.constructor = func
return func
};
JavaScript中的異步機(jī)制可以分為以下幾種:
console.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 調(diào)用 setTimeout 函數(shù),并定義其完成后執(zhí)行的回調(diào)函數(shù)
console.log('script end') //3. 打印 script start
// 輸出順序:script start->script end->settimeout
Promise本身是同步的立即執(zhí)行函數(shù), 當(dāng)在executor中執(zhí)行resolve或者reject的時(shí)候, 此時(shí)是異步操作, 會(huì)先執(zhí)行then/catch等,當(dāng)主棧完成后,才會(huì)去調(diào)用resolve/reject中存放的方法執(zhí)行,打印p的時(shí)候,是打印的返回結(jié)果,一個(gè)Promise實(shí)例。
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 輸出順序: script start->promise1->promise1 end->script end->promise2->settimeout
當(dāng)JS主線程執(zhí)行到Promise對(duì)象時(shí):
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 輸出順序:script start->async1 start->async2->script end->async1 end
async 函數(shù)返回一個(gè) Promise 對(duì)象,當(dāng)函數(shù)執(zhí)行的時(shí)候,一旦遇到 await 就會(huì)先返回,等到觸發(fā)的異步操作完成,再執(zhí)行函數(shù)體內(nèi)后面的語(yǔ)句??梢岳斫鉃椋亲尦隽司€程,跳出了 async 函數(shù)體。
例如:
async function func1() {
return 1
}
console.log(func1())
func1的運(yùn)行結(jié)果其實(shí)就是一個(gè)Promise對(duì)象。因此也可以使用then來(lái)處理后續(xù)邏輯。
func1().then(res => {
console.log(res); // 30
})
await的含義為等待,也就是 async 函數(shù)需要等待await后的函數(shù)執(zhí)行完成并且有了返回結(jié)果(Promise對(duì)象)之后,才能繼續(xù)執(zhí)行下面的代碼。await通過(guò)返回一個(gè)Promise對(duì)象來(lái)實(shí)現(xiàn)同步的效果。
Promise是異步編程的一種解決方案,它是一個(gè)對(duì)象,可以獲取異步操作的消息,他的出現(xiàn)大大改善了異步編程的困境,避免了地獄回調(diào),它比傳統(tǒng)的解決方案回調(diào)函數(shù)和事件更合理和更強(qiáng)大。
所謂Promise,簡(jiǎn)單說(shuō)就是一個(gè)容器,里面保存著某個(gè)未來(lái)才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。從語(yǔ)法上說(shuō),Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。
(1)Promise的實(shí)例有三個(gè)狀態(tài):
當(dāng)把一件事情交給promise時(shí),它的狀態(tài)就是Pending,任務(wù)完成了狀態(tài)就變成了Resolved、沒(méi)有完成失敗了就變成了Rejected。
(2)Promise的實(shí)例有兩個(gè)過(guò)程:
注意:一旦從進(jìn)行狀態(tài)變成為其他狀態(tài)就永遠(yuǎn)不能更改狀態(tài)了。
Promise的特點(diǎn):
pending
?(進(jìn)行中)、?fulfilled
?(已成功)、?rejected
?(已失?。?。只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無(wú)法改變這個(gè)狀態(tài),這也是promise這個(gè)名字的由來(lái)——“承諾”;pending
?變?yōu)?nbsp;?fulfilled
?,從 ?pending
?變?yōu)?nbsp;?rejected
?。這時(shí)就稱(chēng)為 ?resolved
?(已定型)。如果改變已經(jīng)發(fā)生了,你再對(duì)promise對(duì)象添加回調(diào)函數(shù),也會(huì)立即得到這個(gè)結(jié)果。這與事件(event)完全不同,事件的特點(diǎn)是:如果你錯(cuò)過(guò)了它,再去監(jiān)聽(tīng)是得不到結(jié)果的。Promise的缺點(diǎn):
總結(jié):
Promise 對(duì)象是異步編程的一種解決方案,最早由社區(qū)提出。Promise 是一個(gè)構(gòu)造函數(shù),接收一個(gè)函數(shù)作為參數(shù),返回一個(gè) Promise 實(shí)例。一個(gè) Promise 實(shí)例有三種狀態(tài),分別是pending、resolved 和 rejected,分別代表了進(jìn)行中、已成功和已失敗。實(shí)例的狀態(tài)只能由 pending 轉(zhuǎn)變 resolved 或者rejected 狀態(tài),并且狀態(tài)一經(jīng)改變,就凝固了,無(wú)法再被改變了。
狀態(tài)的改變是通過(guò) resolve() 和 reject() 函數(shù)來(lái)實(shí)現(xiàn)的,可以在異步操作結(jié)束后調(diào)用這兩個(gè)函數(shù)改變 Promise 實(shí)例的狀態(tài),它的原型上定義了一個(gè) then 方法,使用這個(gè) then 方法可以為兩個(gè)狀態(tài)的改變注冊(cè)回調(diào)函數(shù)。這個(gè)回調(diào)函數(shù)屬于微任務(wù),會(huì)在本輪事件循環(huán)的末尾執(zhí)行。
注意:在構(gòu)造 Promise
的時(shí)候,構(gòu)造函數(shù)內(nèi)部的代碼是立即執(zhí)行的
Promise對(duì)象代表一個(gè)異步操作,有三種狀態(tài):pending(進(jìn)行中)、fulfilled(已成功)和rejected(已失?。?。
Promise構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù)的兩個(gè)參數(shù)分別是 resolve
和 reject
。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
一般情況下都會(huì)使用 new Promise()
來(lái)創(chuàng)建promise對(duì)象,但是也可以使用 promise.resolve
和 promise.reject
這兩個(gè)方法:
Promise.resolve(value)
的返回值也是一個(gè)promise對(duì)象,可以對(duì)返回值進(jìn)行.then調(diào)用,代碼如下:
Promise.resolve(11).then(function(value){
console.log(value); // 打印出11
});
resolve(11)
代碼中,會(huì)讓promise對(duì)象進(jìn)入確定(resolve
狀態(tài)),并將參數(shù) 11
傳遞給后面的 then
所指定的 onFulfilled
函數(shù);
創(chuàng)建promise對(duì)象可以使用 new Promise
的形式創(chuàng)建對(duì)象,也可以使用 Promise.resolve(value)
的形式創(chuàng)建promise對(duì)象;
Promise.reject
也是 new Promise
的快捷形式,也創(chuàng)建一個(gè)promise對(duì)象。代碼如下:
Promise.reject(new Error(“我錯(cuò)了,請(qǐng)?jiān)彴常?!?);
就是下面的代碼new Promise的簡(jiǎn)單形式:
new Promise(function(resolve,reject){
reject(new Error("我錯(cuò)了,請(qǐng)?jiān)彴常。?));
});
下面是使用resolve方法和reject方法:
function testPromise(ready) {
return new Promise(function(resolve,reject){
if(ready) {
resolve("hello world");
}else {
reject("No thanks");
}
});
};
// 方法調(diào)用
testPromise(true).then(function(msg){
console.log(msg);
},function(error){
console.log(error);
});
上面的代碼的含義是給 testPromise
方法傳遞一個(gè)參數(shù),返回一個(gè)promise對(duì)象,如果為 true
的話,那么調(diào)用promise對(duì)象中的 resolve()
方法,并且把其中的參數(shù)傳遞給后面的 then
第一個(gè)函數(shù)內(nèi),因此打印出 “hello world
”, 如果為 false
的話,會(huì)調(diào)用promise對(duì)象中的 reject()
方法,則會(huì)進(jìn)入 then
的第二個(gè)函數(shù)內(nèi),會(huì)打印 No thanks
;
Promise有五個(gè)常用的方法:then()、catch()、all()、race()、finally。下面就來(lái)看一下這些方法。
當(dāng)Promise執(zhí)行的內(nèi)容符合成功條件時(shí),調(diào)用 resolve
函數(shù),失敗就調(diào)用 reject
函數(shù)。Promise創(chuàng)建完了,那該如何調(diào)用呢?
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then
方法可以接受兩個(gè)回調(diào)函數(shù)作為參數(shù)。第一個(gè)回調(diào)函數(shù)是Promise對(duì)象的狀態(tài)變?yōu)?nbsp;resolved
時(shí)調(diào)用,第二個(gè)回調(diào)函數(shù)是Promise對(duì)象的狀態(tài)變?yōu)?nbsp;rejected
時(shí)調(diào)用。其中第二個(gè)參數(shù)可以省略。
then
方法返回的是一個(gè)新的Promise實(shí)例(不是原來(lái)那個(gè)Promise實(shí)例)。因此可以采用鏈?zhǔn)綄?xiě)法,即 then
方法后面再調(diào)用另一個(gè)then方法。
當(dāng)要寫(xiě)有順序的異步事件時(shí),需要串行時(shí),可以這樣寫(xiě):
let promise = new Promise((resolve,reject)=>{
ajax('first').success(function(res){
resolve(res);
})
})
promise.then(res=>{
return new Promise((resovle,reject)=>{
ajax('second').success(function(res){
resolve(res)
})
})
}).then(res=>{
return new Promise((resovle,reject)=>{
ajax('second').success(function(res){
resolve(res)
})
})
}).then(res=>{
})
那當(dāng)要寫(xiě)的事件沒(méi)有順序或者關(guān)系時(shí),還如何寫(xiě)呢?可以使用 all
方法來(lái)解決。
Promise對(duì)象除了有then方法,還有一個(gè)catch方法,該方法相當(dāng)于 then
方法的第二個(gè)參數(shù),指向 reject
的回調(diào)函數(shù)。不過(guò) catch
方法還有一個(gè)作用,就是在執(zhí)行 resolve
回調(diào)函數(shù)時(shí),如果出現(xiàn)錯(cuò)誤,拋出異常,不會(huì)停止運(yùn)行,而是進(jìn)入 catch
方法中。
p.then((data) => {
console.log('resolved',data);
},(err) => {
console.log('rejected',err);
}
);
p.then((data) => {
console.log('resolved',data);
}).catch((err) => {
console.log('rejected',err);
});
all
方法可以完成并行任務(wù), 它接收一個(gè)數(shù)組,數(shù)組的每一項(xiàng)都是一個(gè) promise
對(duì)象。當(dāng)數(shù)組中所有的 promise
的狀態(tài)都達(dá)到 resolved
的時(shí)候,all
方法的狀態(tài)就會(huì)變成 resolved
,如果有一個(gè)狀態(tài)變成了 rejected
,那么 all
方法的狀態(tài)就會(huì)變成 rejected
。
javascript
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
console.log(res);
//結(jié)果為:[1,2,3]
})
function all(promises){
return new Promise((resolve,reject) => {
let lens = promises.length;
let count = 0;
let res = [];
for(let i = 0; i < lens; i++){
promises[i].then(val => {
count++;
res.push(val);
if(count === lens){
resolve(res)
}
}).catch(error => {
reject(error)
})
}
})
}
調(diào)用 all
方法時(shí)的結(jié)果成功的時(shí)候是回調(diào)函數(shù)的參數(shù)也是一個(gè)數(shù)組,這個(gè)數(shù)組按順序保存著每一個(gè)promise對(duì)象 resolve
執(zhí)行時(shí)的值。
race
方法和 all
一樣,接受的參數(shù)是一個(gè)每項(xiàng)都是 promise
的數(shù)組,但是與 all
不同的是,當(dāng)最先執(zhí)行完的事件執(zhí)行完之后,就直接返回該 promise
對(duì)象的值。如果第一個(gè) promise
對(duì)象狀態(tài)變成 resolved
,那自身的狀態(tài)變成了 resolved
;反之第一個(gè) promise
變成 rejected
,那自身狀態(tài)就會(huì)變成 rejected
。
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
console.log(res);
//結(jié)果:2
},rej=>{
console.log(rej)};
)
那么 race
方法有什么實(shí)際作用呢?當(dāng)要做一件事,超過(guò)多長(zhǎng)時(shí)間就不做了,可以用這個(gè)方法來(lái)解決:
Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
finally
方法用于指定不管 Promise 對(duì)象最后狀態(tài)如何,都會(huì)執(zhí)行的操作。該方法是 ES2018 引入標(biāo)準(zhǔn)的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中,不管 promise
最后的狀態(tài),在執(zhí)行完 then
或 catch
指定的回調(diào)函數(shù)以后,都會(huì)執(zhí)行 finally
方法指定的回調(diào)函數(shù)。
下面是一個(gè)例子,服務(wù)器使用 Promise 處理請(qǐng)求,然后使用 finally
方法關(guān)掉服務(wù)器。
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
finally
方法的回調(diào)函數(shù)不接受任何參數(shù),這意味著沒(méi)有辦法知道,前面的 Promise 狀態(tài)到底是 fulfilled
還是 rejected
。這表明,finally
方法里面的操作,應(yīng)該是與狀態(tài)無(wú)關(guān)的,不依賴(lài)于 Promise 的執(zhí)行結(jié)果。finally
本質(zhì)上是 then
方法的特例:
promise
.finally(() => {
// 語(yǔ)句
});
// 等同于
promise
.then(
result => {
// 語(yǔ)句
return result;
},
error => {
// 語(yǔ)句
throw error;
}
);
上面代碼中,如果不使用 finally
方法,同樣的語(yǔ)句需要為成功和失敗兩種情況各寫(xiě)一次。有了 finally
方法,則只需要寫(xiě)一次。
在工作中經(jīng)常會(huì)碰到這樣一個(gè)需求,比如我使用ajax發(fā)一個(gè)A請(qǐng)求后,成功后拿到數(shù)據(jù),需要把數(shù)據(jù)傳給B請(qǐng)求;那么需要如下編寫(xiě)代碼:
let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})
上面的代碼有如下缺點(diǎn):
Promise
出現(xiàn)之后,代碼變成這樣:
let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
})
這樣代碼看起了就簡(jiǎn)潔了很多,解決了地獄回調(diào)的問(wèn)題。
(1)Promise.all
Promise.all
可以將多個(gè) Promise
實(shí)例包裝成一個(gè)新的Promise實(shí)例。同時(shí),成功和失敗的返回值是不同的,成功的時(shí)候返回的是一個(gè)結(jié)果數(shù)組,而失敗的時(shí)候則返回最先被reject失敗狀態(tài)的值。
Promise.all中傳入的是數(shù)組,返回的也是是數(shù)組,并且會(huì)將進(jìn)行映射,傳入的promise對(duì)象返回的值是按照順序在數(shù)組中排列的,但是注意的是他們執(zhí)行的順序并不是按照順序的,除非可迭代對(duì)象為空。
需要注意,Promise.all獲得的成功結(jié)果的數(shù)組里面的數(shù)據(jù)順序和Promise.all接收到的數(shù)組順序是一致的,這樣當(dāng)遇到發(fā)送多個(gè)請(qǐng)求并根據(jù)請(qǐng)求順序獲取和使用數(shù)據(jù)的場(chǎng)景,就可以使用Promise.all來(lái)解決。
(2)Promise.race
顧名思義,Promse.race就是賽跑的意思,意思就是說(shuō),Promise.race([p1, p2, p3])里面哪個(gè)結(jié)果獲得的快,就返回那個(gè)結(jié)果,不管結(jié)果本身是成功狀態(tài)還是失敗狀態(tài)。當(dāng)要做一件事,超過(guò)多長(zhǎng)時(shí)間就不做了,可以用這個(gè)方法來(lái)解決:
Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
async/await其實(shí)是 Generator
的語(yǔ)法糖,它能實(shí)現(xiàn)的效果都能用then鏈來(lái)實(shí)現(xiàn),它是為優(yōu)化then鏈而開(kāi)發(fā)出來(lái)的。從字面上來(lái)看,async是“異步”的簡(jiǎn)寫(xiě),await則為等待,所以很好理解async 用于申明一個(gè) function 是異步的,而 await 用于等待一個(gè)異步方法執(zhí)行完成。當(dāng)然語(yǔ)法上強(qiáng)制規(guī)定await只能出現(xiàn)在asnyc函數(shù)中,先來(lái)看看async函數(shù)返回了什么:
async function testAsy(){
return 'hello world';
}
let result = testAsy();
console.log(result)
所以,async 函數(shù)返回的是一個(gè) Promise 對(duì)象。async 函數(shù)(包含函數(shù)語(yǔ)句、函數(shù)表達(dá)式、Lambda表達(dá)式)會(huì)返回一個(gè) Promise 對(duì)象,如果在函數(shù)中 return
一個(gè)直接量,async 會(huì)把這個(gè)直接量通過(guò) Promise.resolve()
封裝成 Promise 對(duì)象。
async 函數(shù)返回的是一個(gè) Promise 對(duì)象,所以在最外層不能用 await 獲取其返回值的情況下,當(dāng)然應(yīng)該用原來(lái)的方式:then()
鏈來(lái)處理這個(gè) Promise 對(duì)象,就像這樣:
async function testAsy(){
return 'hello world'
}
let result = testAsy()
console.log(result)
result.then(v=>{
console.log(v) // hello world
})
那如果 async 函數(shù)沒(méi)有返回值,又該如何?很容易想到,它會(huì)返回 Promise.resolve(undefined)
。
聯(lián)想一下 Promise 的特點(diǎn)——無(wú)等待,所以在沒(méi)有 await
的情況下執(zhí)行 async 函數(shù),它會(huì)立即執(zhí)行,返回一個(gè) Promise 對(duì)象,并且,絕不會(huì)阻塞后面的語(yǔ)句。這和普通返回 Promise 對(duì)象的函數(shù)并無(wú)二致。
注意:Promise.resolve(x)
可以看作是 new Promise(resolve => resolve(x))
的簡(jiǎn)寫(xiě),可以用于快速封裝字面量對(duì)象或其他對(duì)象,將其封裝成 Promise 實(shí)例。
await 在等待什么呢?一般來(lái)說(shuō),都認(rèn)為 await 是在等待一個(gè) async 函數(shù)完成。不過(guò)按語(yǔ)法說(shuō)明,await 等待的是一個(gè)表達(dá)式,這個(gè)表達(dá)式的計(jì)算結(jié)果是 Promise 對(duì)象或者其它值(換句話說(shuō),就是沒(méi)有特殊限定)。
因?yàn)?async 函數(shù)返回一個(gè) Promise 對(duì)象,所以 await 可以用于等待一個(gè) async 函數(shù)的返回值——這也可以說(shuō)是 await 在等 async 函數(shù),但要清楚,它等的實(shí)際是一個(gè)返回值。注意到 await 不僅僅用于等 Promise 對(duì)象,它可以等任意表達(dá)式的結(jié)果,所以,await 后面實(shí)際是可以接普通函數(shù)調(diào)用或者直接量的。所以下面這個(gè)示例完全可以正確運(yùn)行:
function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();
await 表達(dá)式的運(yùn)算結(jié)果取決于它等的是什么。
來(lái)看一個(gè)例子:
function testAsy(x){
return new Promise(resolve=>{setTimeout(() => {
resolve(x);
}, 3000)
}
)
}
async function testAwt(){
let result = await testAsy('hello world');
console.log(result); // 3秒鐘之后出現(xiàn)hello world
console.log('cuger') // 3秒鐘之后出現(xiàn)cug
}
testAwt();
console.log('cug') //立即輸出cug
這就是 await 必須用在 async 函數(shù)中的原因。async 函數(shù)調(diào)用不會(huì)造成阻塞,它內(nèi)部所有的阻塞都被封裝在一個(gè) Promise 對(duì)象中異步執(zhí)行。await暫停當(dāng)前async的執(zhí)行,所以'cug''最先輸出,hello world'和‘cuger’是3秒鐘后同時(shí)出現(xiàn)的。
單一的 Promise 鏈并不能發(fā)現(xiàn) async/await 的優(yōu)勢(shì),但是,如果需要處理由多個(gè) Promise 組成的 then 鏈的時(shí)候,優(yōu)勢(shì)就能體現(xiàn)出來(lái)了(很有意思,Promise 通過(guò) then 鏈來(lái)解決多層回調(diào)的問(wèn)題,現(xiàn)在又用 async/await 來(lái)進(jìn)一步優(yōu)化它)。
假設(shè)一個(gè)業(yè)務(wù),分多個(gè)步驟完成,每個(gè)步驟都是異步的,而且依賴(lài)于上一個(gè)步驟的結(jié)果。仍然用 setTimeout
來(lái)模擬異步操作:
/**
* 傳入?yún)?shù) n,表示這個(gè)函數(shù)執(zhí)行的時(shí)間(毫秒)
* 執(zhí)行的結(jié)果是 n + 200,這個(gè)值將用于下一步驟
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
現(xiàn)在用 Promise 方式來(lái)實(shí)現(xiàn)這三個(gè)步驟的處理:
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms
如果用 async/await 來(lái)實(shí)現(xiàn)呢,會(huì)是這樣:
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
結(jié)果和之前的 Promise 實(shí)現(xiàn)是一樣的,但是這個(gè)代碼看起來(lái)是不是清晰得多,幾乎跟同步代碼一樣
async function fn(){
try{
let a = await Promise.reject('error')
}catch(error){
console.log(error)
}
}
以下代碼就是一個(gè)回調(diào)函數(shù)的例子:
ajax(url, () => {
// 處理邏輯
})
回調(diào)函數(shù)有一個(gè)致命的弱點(diǎn),就是容易寫(xiě)出回調(diào)地獄(Callback hell)。假設(shè)多個(gè)請(qǐng)求存在依賴(lài)性,可能會(huì)有如下代碼:
ajax(url, () => {
// 處理邏輯
ajax(url1, () => {
// 處理邏輯
ajax(url2, () => {
// 處理邏輯
})
})
})
以上代碼看起來(lái)不利于閱讀和維護(hù),當(dāng)然,也可以把函數(shù)分開(kāi)來(lái)寫(xiě):
function firstAjax() {
ajax(url1, () => {
// 處理邏輯
secondAjax()
})
}
function secondAjax() {
ajax(url2, () => {
// 處理邏輯
})
}
ajax(url, () => {
// 處理邏輯
firstAjax()
})
以上的代碼雖然看上去利于閱讀了,但是還是沒(méi)有解決根本問(wèn)題?;卣{(diào)地獄的根本問(wèn)題就是:
當(dāng)然,回調(diào)函數(shù)還存在著別的幾個(gè)缺點(diǎn),比如不能使用 try catch
捕獲錯(cuò)誤,不能直接 return
。
異步編程當(dāng)然少不了定時(shí)器了,常見(jiàn)的定時(shí)器函數(shù)有 setTimeout
、setInterval
、requestAnimationFrame
。最常用的是 setTimeout
,很多人認(rèn)為 setTimeout
是延時(shí)多久,那就應(yīng)該是多久后執(zhí)行。
其實(shí)這個(gè)觀點(diǎn)是錯(cuò)誤的,因?yàn)?JS 是單線程執(zhí)行的,如果前面的代碼影響了性能,就會(huì)導(dǎo)致 setTimeout
不會(huì)按期執(zhí)行。當(dāng)然了,可以通過(guò)代碼去修正 setTimeout
,從而使定時(shí)器相對(duì)準(zhǔn)確:
let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
count++
// 代碼執(zhí)行所消耗的時(shí)間
let offset = new Date().getTime() - (startTime + count * interval);
let diff = end - new Date().getTime()
let h = Math.floor(diff / (60 * 1000 * 60))
let hdiff = diff % (60 * 1000 * 60)
let m = Math.floor(hdiff / (60 * 1000))
let mdiff = hdiff % (60 * 1000)
let s = mdiff / (1000)
let sCeil = Math.ceil(s)
let sFloor = Math.floor(s)
// 得到下一次循環(huán)所消耗的時(shí)間
currentInterval = interval - offset
console.log('時(shí):'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執(zhí)行時(shí)間:'+offset, '下次循環(huán)間隔'+currentInterval)
setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)
接下來(lái)看 setInterval
,其實(shí)這個(gè)函數(shù)作用和 setTimeout
基本一致,只是該函數(shù)是每隔一段時(shí)間執(zhí)行一次回調(diào)函數(shù)。
通常來(lái)說(shuō)不建議使用 setInterval
。第一,它和 setTimeout
一樣,不能保證在預(yù)期的時(shí)間執(zhí)行任務(wù)。第二,它存在執(zhí)行累積的問(wèn)題,請(qǐng)看以下偽代碼
function demo() {
setInterval(function(){
console.log(2)
},1000)
sleep(2000)
}
demo()
以上代碼在瀏覽器環(huán)境中,如果定時(shí)器執(zhí)行過(guò)程中出現(xiàn)了耗時(shí)操作,多個(gè)回調(diào)函數(shù)會(huì)在耗時(shí)操作結(jié)束以后同時(shí)執(zhí)行,這樣可能就會(huì)帶來(lái)性能上的問(wèn)題。
如果有循環(huán)定時(shí)器的需求,其實(shí)完全可以通過(guò) requestAnimationFrame
來(lái)實(shí)現(xiàn):
function setInterval(callback, interval) {
let timer
const now = Date.now
let startTime = now()
let endTime = startTime
const loop = () => {
timer = window.requestAnimationFrame(loop)
endTime = now()
if (endTime - startTime >= interval) {
startTime = endTime = now()
callback(timer)
}
}
timer = window.requestAnimationFrame(loop)
return timer
}
let a = 0
setInterval(timer => {
console.log(1)
a++
if (a === 3) cancelAnimationFrame(timer)
}, 1000)
首先 requestAnimationFrame
自帶函數(shù)節(jié)流功能,基本可以保證在 16.6 毫秒內(nèi)只執(zhí)行一次(不掉幀的情況下),并且該函數(shù)的延時(shí)效果是精確的,沒(méi)有其他定時(shí)器時(shí)間不準(zhǔn)的問(wèn)題,當(dāng)然你也可以通過(guò)該函數(shù)來(lái)實(shí)現(xiàn) setTimeout
。
一般使用字面量的形式直接創(chuàng)建對(duì)象,但是這種創(chuàng)建方式對(duì)于創(chuàng)建大量相似對(duì)象的時(shí)候,會(huì)產(chǎn)生大量的重復(fù)代碼。但 js和一般的面向?qū)ο蟮恼Z(yǔ)言不同,在 ES6 之前它沒(méi)有類(lèi)的概念。但是可以使用函數(shù)來(lái)進(jìn)行模擬,從而產(chǎn)生出可復(fù)用的對(duì)象創(chuàng)建方式,常見(jiàn)的有以下幾種:
(1)第一種是工廠模式,工廠模式的主要工作原理是用函數(shù)來(lái)封裝創(chuàng)建對(duì)象的細(xì)節(jié),從而通過(guò)調(diào)用函數(shù)來(lái)達(dá)到復(fù)用的目的。但是它有一個(gè)很大的問(wèn)題就是創(chuàng)建出來(lái)的對(duì)象無(wú)法和某個(gè)類(lèi)型聯(lián)系起來(lái),它只是簡(jiǎn)單的封裝了復(fù)用代碼,而沒(méi)有建立起對(duì)象和類(lèi)型間的關(guān)系。
(2)第二種是構(gòu)造函數(shù)模式。js 中每一個(gè)函數(shù)都可以作為構(gòu)造函數(shù),只要一個(gè)函數(shù)是通過(guò) new 來(lái)調(diào)用的,那么就可以把它稱(chēng)為構(gòu)造函數(shù)。執(zhí)行構(gòu)造函數(shù)首先會(huì)創(chuàng)建一個(gè)對(duì)象,然后將對(duì)象的原型指向構(gòu)造函數(shù)的 prototype 屬性,然后將執(zhí)行上下文中的 this 指向這個(gè)對(duì)象,最后再執(zhí)行整個(gè)函數(shù),如果返回值不是對(duì)象,則返回新建的對(duì)象。因?yàn)?this 的值指向了新建的對(duì)象,因此可以使用 this 給對(duì)象賦值。構(gòu)造函數(shù)模式相對(duì)于工廠模式的優(yōu)點(diǎn)是,所創(chuàng)建的對(duì)象和構(gòu)造函數(shù)建立起了聯(lián)系,因此可以通過(guò)原型來(lái)識(shí)別對(duì)象的類(lèi)型。但是構(gòu)造函數(shù)存在一個(gè)缺點(diǎn)就是,造成了不必要的函數(shù)對(duì)象的創(chuàng)建,因?yàn)樵? js 中函數(shù)也是一個(gè)對(duì)象,因此如果對(duì)象屬性中如果包含函數(shù)的話,那么每次都會(huì)新建一個(gè)函數(shù)對(duì)象,浪費(fèi)了不必要的內(nèi)存空間,因?yàn)楹瘮?shù)是所有的實(shí)例都可以通用的。
(3)第三種模式是原型模式,因?yàn)槊恳粋€(gè)函數(shù)都有一個(gè) prototype 屬性,這個(gè)屬性是一個(gè)對(duì)象,它包含了通過(guò)構(gòu)造函數(shù)創(chuàng)建的所有實(shí)例都能共享的屬性和方法。因此可以使用原型對(duì)象來(lái)添加公用屬性和方法,從而實(shí)現(xiàn)代碼的復(fù)用。這種方式相對(duì)于構(gòu)造函數(shù)模式來(lái)說(shuō),解決了函數(shù)對(duì)象的復(fù)用問(wèn)題。但是這種模式也存在一些問(wèn)題,一個(gè)是沒(méi)有辦法通過(guò)傳入?yún)?shù)來(lái)初始化值,另一個(gè)是如果存在一個(gè)引用類(lèi)型如 Array 這樣的值,那么所有的實(shí)例將共享一個(gè)對(duì)象,一個(gè)實(shí)例對(duì)引用類(lèi)型值的改變會(huì)影響所有的實(shí)例。
(4)第四種模式是組合使用構(gòu)造函數(shù)模式和原型模式,這是創(chuàng)建自定義類(lèi)型的最常見(jiàn)方式。因?yàn)闃?gòu)造函數(shù)模式和原型模式分開(kāi)使用都存在一些問(wèn)題,因此可以組合使用這兩種模式,通過(guò)構(gòu)造函數(shù)來(lái)初始化對(duì)象的屬性,通過(guò)原型對(duì)象來(lái)實(shí)現(xiàn)函數(shù)方法的復(fù)用。這種方法很好的解決了兩種模式單獨(dú)使用時(shí)的缺點(diǎn),但是有一點(diǎn)不足的就是,因?yàn)槭褂昧藘煞N不同的模式,所以對(duì)于代碼的封裝性不夠好。
(5)第五種模式是動(dòng)態(tài)原型模式,這一種模式將原型方法賦值的創(chuàng)建過(guò)程移動(dòng)到了構(gòu)造函數(shù)的內(nèi)部,通過(guò)對(duì)屬性是否存在的判斷,可以實(shí)現(xiàn)僅在第一次調(diào)用函數(shù)時(shí)對(duì)原型對(duì)象賦值一次的效果。這一種方式很好地對(duì)上面的混合模式進(jìn)行了封裝。
(6)第六種模式是寄生構(gòu)造函數(shù)模式,這一種模式和工廠模式的實(shí)現(xiàn)基本相同,我對(duì)這個(gè)模式的理解是,它主要是基于一個(gè)已有的類(lèi)型,在實(shí)例化時(shí)對(duì)實(shí)例化的對(duì)象進(jìn)行擴(kuò)展。這樣既不用修改原來(lái)的構(gòu)造函數(shù),也達(dá)到了擴(kuò)展對(duì)象的目的。它的一個(gè)缺點(diǎn)和工廠模式一樣,無(wú)法實(shí)現(xiàn)對(duì)象的識(shí)別。
(1)第一種是以原型鏈的方式來(lái)實(shí)現(xiàn)繼承,但是這種實(shí)現(xiàn)方式存在的缺點(diǎn)是,在包含有引用類(lèi)型的數(shù)據(jù)時(shí),會(huì)被所有的實(shí)例對(duì)象所共享,容易造成修改的混亂。還有就是在創(chuàng)建子類(lèi)型的時(shí)候不能向超類(lèi)型傳遞參數(shù)。
(2)第二種方式是使用借用構(gòu)造函數(shù)的方式,這種方式是通過(guò)在子類(lèi)型的函數(shù)中調(diào)用超類(lèi)型的構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)的,這一種方法解決了不能向超類(lèi)型傳遞參數(shù)的缺點(diǎn),但是它存在的一個(gè)問(wèn)題就是無(wú)法實(shí)現(xiàn)函數(shù)方法的復(fù)用,并且超類(lèi)型原型定義的方法子類(lèi)型也沒(méi)有辦法訪問(wèn)到。
(3)第三種方式是組合繼承,組合繼承是將原型鏈和借用構(gòu)造函數(shù)組合起來(lái)使用的一種方式。通過(guò)借用構(gòu)造函數(shù)的方式來(lái)實(shí)現(xiàn)類(lèi)型的屬性的繼承,通過(guò)將子類(lèi)型的原型設(shè)置為超類(lèi)型的實(shí)例來(lái)實(shí)現(xiàn)方法的繼承。這種方式解決了上面的兩種模式單獨(dú)使用時(shí)的問(wèn)題,但是由于我們是以超類(lèi)型的實(shí)例來(lái)作為子類(lèi)型的原型,所以調(diào)用了兩次超類(lèi)的構(gòu)造函數(shù),造成了子類(lèi)型的原型中多了很多不必要的屬性。
(4)第四種方式是原型式繼承,原型式繼承的主要思路就是基于已有的對(duì)象來(lái)創(chuàng)建新的對(duì)象,實(shí)現(xiàn)的原理是,向函數(shù)中傳入一個(gè)對(duì)象,然后返回一個(gè)以這個(gè)對(duì)象為原型的對(duì)象。這種繼承的思路主要不是為了實(shí)現(xiàn)創(chuàng)造一種新的類(lèi)型,只是對(duì)某個(gè)對(duì)象實(shí)現(xiàn)一種簡(jiǎn)單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實(shí)現(xiàn)。缺點(diǎn)與原型鏈方式相同。
(5)第五種方式是寄生式繼承,寄生式繼承的思路是創(chuàng)建一個(gè)用于封裝繼承過(guò)程的函數(shù),通過(guò)傳入一個(gè)對(duì)象,然后復(fù)制一個(gè)對(duì)象的副本,然后對(duì)象進(jìn)行擴(kuò)展,最后返回這個(gè)對(duì)象。這個(gè)擴(kuò)展的過(guò)程就可以理解是一種繼承。這種繼承的優(yōu)點(diǎn)就是對(duì)一個(gè)簡(jiǎn)單對(duì)象實(shí)現(xiàn)繼承,如果這個(gè)對(duì)象不是自定義類(lèi)型時(shí)。缺點(diǎn)是沒(méi)有辦法實(shí)現(xiàn)函數(shù)的復(fù)用。
(6)第六種方式是寄生式組合繼承,組合繼承的缺點(diǎn)就是使用超類(lèi)型的實(shí)例做為子類(lèi)型的原型,導(dǎo)致添加了不必要的原型屬性。寄生式組合繼承的方式是使用超類(lèi)型的原型的副本來(lái)作為子類(lèi)型的原型,這樣就避免了創(chuàng)建不必要的屬性。
垃圾回收:JavaScript代碼運(yùn)行時(shí),需要分配內(nèi)存空間來(lái)儲(chǔ)存變量和值。當(dāng)變量不在參與運(yùn)行時(shí),就需要系統(tǒng)收回被占用的內(nèi)存空間,這就是垃圾回收。
回收機(jī)制:
瀏覽器通常使用的垃圾回收方法有兩種:標(biāo)記清除,引用計(jì)數(shù)。
1)標(biāo)記清除
2)引用計(jì)數(shù)
function fun() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
這種情況下,就要手動(dòng)釋放變量占用的內(nèi)存:
obj1.a = null
obj2.a = null
雖然瀏覽器可以進(jìn)行垃圾自動(dòng)回收,但是當(dāng)代碼比較復(fù)雜時(shí),垃圾回收所帶來(lái)的代價(jià)比較大,所以應(yīng)該盡量減少垃圾回收。
以下四種情況會(huì)造成內(nèi)存的泄漏:
更多建議: