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