JS 中分爲七種內置類型,七種內置類型又分爲兩大類型:基本類型和對象(Object)。html
基本類型有六種: null
,undefined
,boolean
,number
,string
,symbol
。c++
其中 JS 的數字類型是浮點類型的,沒有整型。而且浮點類型基於 IEEE 754標準實現,在使用中會遇到某些 Bug。NaN
也屬於 number
類型,而且 NaN
不等於自身。git
對於基本類型來講,若是使用字面量的方式,那麼這個變量只是個字面量,只有在必要的時候纔會轉換爲對應的類型github
let a = 111 // 這只是字面量,不是 number 類型
a.toString() // 使用時候纔會轉換爲對象類型
複製代碼
對象(Object)是引用類型,在使用過程當中會遇到淺拷貝和深拷貝的問題。面試
let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF
複製代碼
typeof
對於基本類型,除了 null
均可以顯示正確的類型正則表達式
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 沒有聲明,可是還會顯示 undefined
複製代碼
typeof
對於對象,除了函數都會顯示 object
算法
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
複製代碼
對於 null
來講,雖然它是基本類型,可是會顯示 object
,這是一個存在好久了的 Bug編程
typeof null // 'object'
複製代碼
PS:爲何會出現這種狀況呢?由於在 JS 的最第一版本中,使用的是 32 位系統,爲了性能考慮使用低位存儲了變量的類型信息,000
開頭表明是對象,然而 null
表示爲全零,因此將它錯誤的判斷爲 object
。雖然如今的內部類型判斷代碼已經改變了,可是對於這個 Bug 倒是一直流傳下來。數組
若是咱們想得到一個變量的正確類型,能夠經過 Object.prototype.toString.call(xx)
。這樣咱們就能夠得到相似 [object Type]
的字符串。promise
let a
// 咱們也能夠這樣判斷 undefined
a === undefined
// 可是 undefined 不是保留字,可以在低版本瀏覽器被賦值
let undefined = 1
// 這樣判斷就會出錯
// 因此能夠用下面的方式來判斷,而且代碼量更少
// 由於 void 後面隨便跟上一個組成表達式
// 返回就是 undefined
a === void 0
複製代碼
在條件判斷時,除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其餘全部值都轉爲 true
,包括全部對象。
對象在轉換基本類型時,首先會調用 valueOf
而後調用 toString
。而且這兩個方法你是能夠重寫的。
let a = {
valueOf() {
return 0
}
}
複製代碼
固然你也能夠重寫 Symbol.toPrimitive
,該方法在轉基本類型時調用優先級最高。
let a = {
valueOf() {
return 0;
},
toString() {
return '1';
},
[Symbol.toPrimitive]() {
return 2;
}
}
1 + a // => 3
'1' + a // => '12'
複製代碼
只有當加法運算時,其中一方是字符串類型,就會把另外一個也轉爲字符串類型。其餘運算只要其中一方是數字,那麼另外一方就轉爲數字。而且加法運算會觸發三種類型轉換:將值轉換爲原始值,轉換爲數字,轉換爲字符串。
1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
複製代碼
對於加號須要注意這個表達式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
// 由於 + 'b' -> NaN
// 你也許在一些代碼中看到過 + '1' -> 1
複製代碼
==
操做符上圖中的 toPrimitive
就是對象轉基本類型。
這裏來解析一道題目 [] == ![] // -> true
,下面是這個表達式爲什麼爲 true
的步驟
// [] 轉成 true,而後取反變成 false
[] == false
// 根據第 8 條得出
[] == ToNumber(false)
[] == 0
// 根據第 10 條得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據第 6 條得出
0 == 0 // -> true
複製代碼
toPrimitive
轉換對象unicode
字符索引來比較每一個函數都有 prototype
屬性,除了 Function.prototype.bind()
,該屬性指向原型。
每一個對象都有 __proto__
屬性,指向了建立該對象的構造函數的原型。其實這個屬性指向了 [[prototype]]
,可是 [[prototype]]
是內部屬性,咱們並不能訪問到,因此使用 _proto_
來訪問。
對象能夠經過 __proto__
來尋找不屬於該對象的屬性,__proto__
將對象鏈接起來組成了原型鏈。
若是你想更進一步的瞭解原型,能夠仔細閱讀 深度解析原型中的各個難點。
在調用 new
的過程當中會發生以上四件事情,咱們也能夠試着來本身實現一個 new
function create() {
// 建立一個空的對象
let obj = new Object()
// 得到構造函數
let Con = [].shift.call(arguments)
// 連接到原型
obj.__proto__ = Con.prototype
// 綁定 this,執行構造函數
let result = Con.apply(obj, arguments)
// 確保 new 出來的是個對象
return typeof result === 'object' ? result : obj
}
複製代碼
對於實例對象來講,都是經過 new
產生的,不管是 function Foo()
仍是 let a = { b : 1 }
。
對於建立一個對象來講,更推薦使用字面量的方式建立對象(不管性能上仍是可讀性)。由於你使用 new Object()
的方式建立對象須要經過做用域鏈一層層找到 Object
,可是你使用字面量的方式就沒這個問題。
function Foo() {}
// function 就是個語法糖
// 內部等同於 new Function()
let a = { b: 1 }
// 這個字面量內部也是使用了 new Object()
複製代碼
對於 new
來講,還須要注意下運算符優先級。
function Foo() {
return this;
}
Foo.getName = function () {
console.log('1');
};
Foo.prototype.getName = function () {
console.log('2');
};
new Foo.getName(); // -> 1
new Foo().getName(); // -> 2
複製代碼
從上圖能夠看出,new Foo()
的優先級大於 new Foo
,因此對於上述代碼來講能夠這樣劃分執行順序
new (Foo.getName());
(new Foo()).getName();
複製代碼
對於第一個函數來講,先執行了 Foo.getName()
,因此結果爲 1;對於後者來講,先執行 new Foo()
產生了一個實例,而後經過原型鏈找到了 Foo
上的 getName
函數,因此結果爲 2。
instanceof
能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈中是否是能找到類型的 prototype
。
咱們也能夠試着實現一下 instanceof
function instanceof(left, right) {
// 得到類型的原型
let prototype = right.prototype
// 得到對象的原型
left = left.__proto__
// 判斷對象的類型是否等於類型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
複製代碼
this
是不少人會混淆的概念,可是其實他一點都不難,你只須要記住幾個規則就能夠了。
function foo() {
console.log(this.a)
}
var a = 1
foo()
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上二者狀況 `this` 只依賴於調用函數前的對象,優先級是第二個狀況大於第一個狀況
// 如下狀況是優先級最高的,`this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 還有種就是利用 call,apply,bind 改變 this,這個優先級僅次於 new
複製代碼
以上幾種狀況明白了,不少代碼中的 this
應該就沒什麼問題了,下面讓咱們看看箭頭函數中的 this
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
複製代碼
箭頭函數實際上是沒有 this
的,這個函數中的 this
只取決於他外面的第一個不是箭頭函數的函數的 this
。在這個例子中,由於調用 a
符合前面代碼中的第一個狀況,因此 this
是 window
。而且 this
一旦綁定了上下文,就不會被任何代碼改變。
當執行 JS 代碼時,會產生三種執行上下文
每一個執行上下文中都有三個重要的屬性
var a = 10
function foo(i) {
var b = 20
}
foo()
複製代碼
對於上述代碼,執行棧中有兩個上下文:全局上下文和函數 foo
上下文。
stack = [
globalContext,
fooContext
]
複製代碼
對於全局上下文來講,VO 大概是這樣的
globalContext.VO === globe
globalContext.VO = {
a: undefined,
foo: <Function>, } 複製代碼
對於函數 foo
來講,VO 不能訪問,只能訪問到活動對象(AO)
fooContext.VO === foo.AO
fooContext.AO {
i: undefined,
b: undefined,
arguments: <> } // arguments 是函數獨有的對象(箭頭函數沒有) // 該對象是一個僞數組,有 `length` 屬性且能夠經過下標訪問元素 // 該對象中的 `callee` 屬性表明函數自己 // `caller` 屬性表明函數的調用者 複製代碼
對於做用域鏈,能夠把它理解成包含自身變量對象和上級變量對象的列表,經過 [[Scope]]
屬性查找上級變量
fooContext.[[Scope]] = [
globalContext.VO
]
fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
fooContext.Scope = [
fooContext.VO,
globalContext.VO
]
複製代碼
接下來讓咱們看一個老生常談的例子,var
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
複製代碼
想必以上的輸出你們確定都已經明白了,這是由於函數和變量提高的緣由。一般提高的解釋是說將聲明的代碼移動到了頂部,這其實沒有什麼錯誤,便於你們理解。可是更準確的解釋應該是:在生成執行上下文時,會有兩個階段。第一個階段是建立的階段(具體步驟是建立 VO),JS 解釋器會找出須要提高的變量和函數,而且給他們提早在內存中開闢好空間,函數的話會將整個函數存入內存中,變量只聲明而且賦值爲 undefined,因此在第二個階段,也就是代碼執行階段,咱們能夠直接提早使用。
在提高的過程當中,相同的函數會覆蓋上一個函數,而且函數優先於變量提高
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
複製代碼
var
會產生不少錯誤,因此在 ES6中引入了 let
。let
不能在聲明前使用,可是這並非常說的 let
不會提高,let
提高了聲明但沒有賦值,由於臨時死區致使了並不能在聲明前使用。
對於非匿名的當即執行函數須要注意如下一點
var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
複製代碼
由於當 JS 解釋器在遇到非匿名的當即執行函數時,會建立一個輔助的特定對象,而後將函數名稱做爲這個對象的屬性,所以函數內部才能夠訪問到 foo
,可是這個值又是隻讀的,因此對它的賦值並不生效,因此打印的結果仍是這個函數,而且外部的值也沒有發生更改。
specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // remove specialObject from the front of scope chain
複製代碼
閉包的定義很簡單:函數 A 返回了一個函數 B,而且函數 B 中使用了函數 A 的變量,函數 B 就被稱爲閉包。
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
複製代碼
你是否會疑惑,爲何函數 A 已經彈出調用棧了,爲何函數 B 還能引用到函數 A 中的變量。由於函數 A 中的變量這時候是存儲在堆上的。如今的 JS 引擎能夠經過逃逸分析辨別出哪些變量須要存儲在堆上,哪些須要存儲在棧上。
經典面試題,循環中使用閉包解決 var
定義函數的問題
for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
複製代碼
首先由於 setTimeout
是個異步函數,全部會先把循環所有執行完畢,這時候 i
就是 6 了,因此會輸出一堆 6。
解決辦法兩種,第一種使用閉包
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
複製代碼
第二種就是使用 setTimeout
的第三個參數
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 );
}
複製代碼
由於對於 let
來講,他會建立一個塊級做用域,至關於
{ // 造成塊級做用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
複製代碼
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
複製代碼
從上述例子中咱們能夠發現,若是給一個變量賦值一個對象,那麼二者的值會是同一個引用,其中一方改變,另外一方也會相應改變。
一般在開發中咱們不但願出現這樣的問題,咱們可使用淺拷貝來解決這個問題。
首先能夠經過 Object.assign
來解決這個問題。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
複製代碼
固然咱們也能夠經過展開運算符(…)來解決
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1
複製代碼
一般淺拷貝就能解決大部分問題了,可是當咱們遇到以下狀況就須要使用到深拷貝了
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native
複製代碼
淺拷貝只解決了第一層的問題,若是接下去的值中還有對象的話,那麼就又回到剛開始的話題了,二者享有相同的引用。要解決這個問題,咱們須要引入深拷貝。
這個問題一般能夠經過 JSON.parse(JSON.stringify(object))
來解決。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
複製代碼
可是該方法也是有侷限性的:
undefined
symbol
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
複製代碼
若是你有這麼一個循環引用對象,你會發現你不能經過該方法深拷貝
在遇到函數、 undefined
或者 symbol
的時候,該對象也不能正常的序列化
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function() {},
name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
複製代碼
你會發如今上述狀況中,該方法會忽略掉函數和 undefined
。
可是在一般狀況下,複雜數據都是能夠序列化的,因此這個函數能夠解決大部分問題,而且該函數是內置函數中處理深拷貝性能最快的。固然若是你的數據中含有以上三種狀況下,可使用 lodash 的深拷貝函數。
若是你所需拷貝的對象含有內置類型而且不包含函數,可使用 MessageChannel
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
var obj = {a: 1, b: {
c: b
}}
// 注意該方法是異步的
// 能夠處理 undefined 和循環引用對象
(async () => {
const clone = await structuralClone(obj)
})()
複製代碼
在有 Babel 的狀況下,咱們能夠直接使用 ES6 的模塊化
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}
import {a, b} from './a.js'
import XXX from './b.js'
複製代碼
CommonJs
是 Node 獨有的規範,瀏覽器中使用就須要用到 Browserify
解析了。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
複製代碼
在上述代碼中,module.exports
和 exports
很容易混淆,讓咱們來看看大體內部實現
var module = require('./a.js')
module.a
// 這裏其實就是包裝了一層當即執行函數,這樣就不會污染全局變量了,
// 重要的是 module 這裏,module 是 Node 獨有的一個變量
module.exports = {
a: 1
}
// 基本實現
var module = {
exports: {} // exports 就是個空對象
}
// 這個是爲何 exports 和 module.exports 用法類似的緣由
var exports = module.exports
var load = function (module) {
// 導出的東西
var a = 1
module.exports = a
return module.exports
};
複製代碼
再來講說 module.exports
和 exports
,用法實際上是類似的,可是不能對 exports
直接賦值,不會有任何效果。
對於 CommonJS
和 ES6 中的模塊化的二者區別是:
前者支持動態導入,也就是 require(${path}/xx.js)
,後者目前不支持,可是已有提案
前者是同步導入,由於用於服務端,文件都在本地,同步導入即便卡住主線程影響也不大。然後者是異步導入,由於用於瀏覽器,須要下載文件,若是也採用同步導入會對渲染有很大影響
前者在導出時都是值拷貝,就算導出的值變了,導入的值也不會改變,因此若是想更新值,必須從新導入一次。可是後者採用實時綁定的方式,導入導出的值都指向同一個內存地址,因此導入值會跟隨導出值變化
後者會編譯成 require/exports
來執行的
AMD 是由 RequireJS
提出的
// AMD
define(['./a', './b'], function(a, b) {
a.do()
b.do()
})
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
複製代碼
你是否在平常開發中遇到一個問題,在滾動事件中須要作個複雜計算或者實現一個按鈕的防二次點擊操做。
這些需求均可以經過函數防抖動來實現。尤爲是第一個需求,若是在頻繁的事件回調中作複雜計算,頗有可能致使頁面卡頓,不如將屢次計算合併爲一次計算,只在一個精確點作操做。
PS:防抖和節流的做用都是防止函數屢次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於wait,防抖的狀況下只會調用一次,而節流的 狀況會每隔必定時間(參數wait)調用函數。
咱們先來看一個袖珍版的防抖理解一下防抖的實現:
// func是用戶傳入須要防抖的函數
// wait是等待時間
const debounce = (func, wait = 50) => {
// 緩存一個定時器id
let timer = 0
// 這裏返回的函數是每次用戶實際調用的防抖函數
// 若是已經設定過定時器了就清空上一次的定時器
// 開始一個新的定時器,延遲執行用戶傳入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不難看出若是用戶調用該函數的間隔小於wait的狀況下,上一次的時間還未到就被清除了,並不會執行函數
複製代碼
這是一個簡單版的防抖,可是有缺陷,這個防抖只能在最後調用。通常的防抖會有immediate選項,表示是否當即調用。這二者的區別,舉個栗子來講:
延遲執行
的防抖函數,它老是在一連串(間隔小於wait的)函數觸發以後調用。當即執行
的防抖函數,它老是在第一次調用,而且下一次調用必須與前一次調用的時間間隔大於wait纔會觸發。下面咱們來實現一個帶有當即執行選項的防抖函數
// 這個是用來獲取當前時間戳的
function now() {
return +new Date()
}
/** * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行 * * @param {function} func 回調函數 * @param {number} wait 表示時間窗口的間隔 * @param {boolean} immediate 設置爲ture時,是否當即調用函數 * @return {function} 返回客戶調用函數 */
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延遲執行函數
const later = () => setTimeout(() => {
// 延遲函數執行完畢,清空緩存的定時器序號
timer = null
// 延遲執行的狀況下,函數會在延遲函數中執行
// 使用到以前緩存的參數和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 這裏返回的函數是每次實際調用的函數
return function(...params) {
// 若是沒有建立延遲執行函數(later),就建立一個
if (!timer) {
timer = later()
// 若是是當即執行,調用函數
// 不然緩存參數和調用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 若是已有延遲執行函數(later),調用的時候清除原來的並從新設定一個
// 這樣作延遲函數會從新計時
} else {
clearTimeout(timer)
timer = later()
}
}
}
複製代碼
總體函數實現的不難,總結一下。
null
,就能夠再次點擊了。防抖動和節流本質是不同的。防抖動是將屢次執行變爲最後一次執行,節流是將屢次執行變成每隔一段時間執行。
/** * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait * * @param {function} func 回調函數 * @param {number} wait 表示時間窗口的間隔 * @param {object} options 若是想忽略開始函數的的調用,傳入{leading: false}。 * 若是想忽略結尾函數的調用,傳入{trailing: false} * 二者不能共存,不然函數不能執行 * @return {function} 返回客戶調用函數 */
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 以前的時間戳
var previous = 0;
// 若是 options 沒傳則設爲空對象
if (!options) options = {};
// 定時器回調函數
var later = function() {
// 若是設置了 leading,就將 previous 設爲 0
// 用於下面函數的第一個 if 判斷
previous = options.leading === false ? 0 : _.now();
// 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 得到當前時間戳
var now = _.now();
// 首次進入前者確定爲 true
// 若是須要第一次不執行函數
// 就將上次時間戳設爲當前的
// 這樣在接下來計算 remaining 的值時會大於0
if (!previous && options.leading === false) previous = now;
// 計算剩餘時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 若是當前調用已經大於上次調用時間 + wait
// 或者用戶手動調了時間
// 若是設置了 trailing,只會進入這個條件
// 若是沒有設置 leading,那麼第一次會進入這個條件
// 還有一點,你可能會以爲開啓了定時器那麼應該不會進入這個 if 條件了
// 其實仍是會進入的,由於定時器的延時
// 並非準確的時間,極可能你設置了2秒
// 可是他須要2.2秒才觸發,這時候就會進入這個條件
if (remaining <= 0 || remaining > wait) {
// 若是存在定時器就清理掉不然會調用二次回調
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判斷是否設置了定時器和 trailing
// 沒有的話就開啓一個定時器
// 而且不能不能同時設置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
複製代碼
在 ES5 中,咱們可使用以下方式解決繼承的問題
function Super() {}
Super.prototype.getNumber = function() {
return 1
}
function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
複製代碼
以上繼承實現思路就是將子類的原型設置爲父類的原型
在 ES6 中,咱們能夠經過 class
語法輕鬆解決這個問題
class MyDate extends Date {
test() {
return this.getTime()
}
}
let myDate = new MyDate()
myDate.test()
複製代碼
可是 ES6 不是全部瀏覽器都兼容,因此咱們須要使用 Babel 來編譯這段代碼。
若是你使用編譯過得代碼調用 myDate.test()
你會驚奇地發現出現了報錯
由於在 JS 底層有限制,若是不是由 Date
構造出來的實例的話,是不能調用 Date
裏的函數的。因此這也側面的說明了:ES6 中的 class
繼承與 ES5 中的通常繼承寫法是不一樣的。
既然底層限制了實例必須由 Date
構造出來,那麼咱們能夠改變下思路實現繼承
function MyData() {
}
MyData.prototype.test = function () {
return this.getTime()
}
let d = new Date()
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
複製代碼
以上繼承實現思路:先建立父類實例 => 改變實例原先的 _proto__
轉而鏈接到子類的 prototype
=> 子類的 prototype
的 __proto__
改成父類的 prototype
。
經過以上方法實現的繼承就能夠完美解決 JS 底層的這個限制。
首先說下前二者的區別。
call
和 apply
都是爲了解決改變 this
的指向。做用都是相同的,只是傳參的方式不一樣。
除了第一個參數外,call
能夠接收一個參數列表,apply
只接受一個參數數組。
let a = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
複製代碼
能夠從如下幾點來考慮如何實現
window
Function.prototype.myCall = function (context) {
var context = context || window
// 給 context 添加一個屬性
// getValue.call(a, 'yck', '24') => a.fn = getValue
context.fn = this
// 將 context 後面的參數取出來
var args = [...arguments].slice(1)
// getValue.call(a, 'yck', '24') => a.fn('yck', '24')
var result = context.fn(...args)
// 刪除 fn
delete context.fn
return result
}
複製代碼
以上就是 call
的思路,apply
的實現也相似
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this
var result
// 須要判斷是否存儲第二個參數
// 若是存在,就將第二個參數展開
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
複製代碼
bind
和其餘兩個方法做用也是一致的,只是該方法會返回一個函數。而且咱們能夠經過 bind
實現柯里化。
一樣的,也來模擬實現下 bind
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一個函數
return function F() {
// 由於返回了一個函數,咱們能夠 new F(),因此須要判斷
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
複製代碼
Promise 是 ES6 新增的語法,解決了回調地獄的問題。
能夠把 Promise 當作一個狀態機。初始是 pending
狀態,能夠經過函數 resolve
和 reject
,將狀態轉變爲 resolved
或者 rejected
狀態,狀態一旦改變就不能再次變化。
then
函數會返回一個 Promise 實例,而且該返回值是一個新的實例而不是以前的實例。由於 Promise 規範規定除了 pending
狀態,其餘狀態是不能夠改變的,若是返回的是一個相同實例的話,多個 then
調用就失去意義了。
對於 then
來講,本質上能夠把它當作是 flatMap
// 三種狀態
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一個函數參數,該函數會當即執行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用於保存 then 中的回調,只有當 promise
// 狀態爲 pending 時纔會緩存,而且每一個實例至多緩存一個
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];
_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 若是 value 是個 Promise,遞歸執行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 異步執行,保證執行順序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};
_this.reject = function (reason) {
setTimeout(() => { // 異步執行,保證執行順序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用於解決如下問題
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}
MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 規範 2.2.7,then 必須返回一個新的 promise
var promise2;
// 規範 2.2.onResolved 和 onRejected 都爲可選參數
// 若是類型不是函數須要忽略,同時也實現了透傳
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;
if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 規範 2.2.4,保證 onFulfilled,onRjected 異步執行
// 因此用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 異步執行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考慮到可能會有報錯,因此使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 規範 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 規範 2.3.1,x 不能和 promise2 相同,避免循環引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 規範 2.3.2
// 若是 x 爲 Promise,狀態爲 pending 須要繼續等待不然執行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次調用該函數是爲了確認 x resolve 的
// 參數是什麼類型,若是是基本類型就再次 resolve
// 把值傳給下個 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 規範 2.3.3.3.3
// reject 或者 resolve 其中一個執行過得話,忽略其餘的
let called = false;
// 規範 2.3.3,判斷 x 是否爲對象或者函數
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 規範 2.3.3.2,若是不能取出 then,就 reject
try {
// 規範 2.3.3.1
let then = x.then;
// 若是 then 是函數,調用 x.then
if (typeof then === "function") {
// 規範 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 規範 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 規範 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 規範 2.3.4,x 爲基本類型
resolve(x);
}
}
複製代碼
以上就是根據 Promise / A+ 規範來實現的代碼,能夠經過 promises-aplus-tests
的完整測試
Generator 是 ES6 中新增的語法,和 Promise 同樣,均可以用來異步編程
// 使用 * 表示這是一個 Generator 函數
// 內部能夠經過 yield 暫停代碼
// 經過調用 next 恢復執行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
複製代碼
從以上代碼能夠發現,加上 *
的函數執行後擁有了 next
函數,也就是說函數執行後返回了一個對象。每次調用 next
函數能夠繼續執行被暫停的代碼。如下是 Generator 函數的簡單實現
// cb 也就是編譯過的 test 函數
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};
return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 若是你使用 babel 編譯後能夠發現 test 函數變成了這樣
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 能夠發現經過 yield 將代碼分割成幾塊
// 每次執行 next 函數就執行一塊代碼
// 而且代表下次須要執行哪塊代碼
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 執行完畢
case 6:
case "end":
return _context.stop();
}
}
});
}
複製代碼
Map
做用是生成一個新數組,遍歷原數組,將每一個元素拿出來作一些變換而後 append
到新的數組中。
[1, 2, 3].map((v) => v + 1)
// -> [2, 3, 4]
複製代碼
Map
有三個參數,分別是當前索引元素,索引,原數組
['1','2','3'].map(parseInt)
// parseInt('1', 0) -> 1
// parseInt('2', 1) -> NaN
// parseInt('3', 2) -> NaN
複製代碼
FlatMap
和 map
的做用幾乎是相同的,可是對於多維數組來講,會將原數組降維。能夠將 FlatMap
當作是 map
+ flatten
,目前該函數在瀏覽器中還不支持。
[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]
複製代碼
若是想將一個多維數組完全的降維,能夠這樣實現
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]
flattenDeep([1, [[2], [3, [4]], 5]])
複製代碼
Reduce
做用是數組中的值組合起來,最終獲得一個值
function a() {
console.log(1);
}
function b() {
console.log(2);
}
[a, b].reduce((a, b) => a(b()))
// -> 2 1
複製代碼
一個函數若是加上 async
,那麼該函數就會返回一個 Promise
async function test() {
return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}
複製代碼
能夠把 async
當作將函數返回值使用 Promise.resolve()
包裹了下。
await
只能在 async
函數中使用
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
console.log('finish')
resolve("sleep");
}, 2000);
});
}
async function test() {
let value = await sleep();
console.log("object");
}
test()
複製代碼
上面代碼會先打印 finish
而後再打印 object
。由於 await
會等待 sleep
函數 resolve
,因此即便後面是同步代碼,也不會先去執行同步代碼再來執行異步代碼。
async 和 await
相比直接使用 Promise
來講,優點在於處理 then
的調用鏈,可以更清晰準確的寫出代碼。缺點在於濫用 await
可能會致使性能問題,由於 await
會阻塞代碼,也許以後的異步代碼並不依賴於前者,但仍然須要等待前者完成,致使代碼失去了併發性。
下面來看一個使用 await
的代碼。
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
複製代碼
對於以上代碼你可能會有疑惑,這裏說明下原理
b
先執行,在執行到 await 10
以前變量 a
仍是 0,由於在 await
內部實現了 generators
,generators
會保留堆棧中東西,因此這時候 a = 0
被保存了下來await
是異步操做,遇到await
就會當即返回一個pending
狀態的Promise
對象,暫時返回執行代碼的控制權,使得函數外的代碼得以繼續執行,因此會先執行 console.log('1', a)
a = 10
Proxy 是 ES6 中新增的功能,能夠用來自定義對象中的操做
let p = new Proxy(target, handler);
// `target` 表明須要添加代理的對象
// `handler` 用來自定義對象中的操做
複製代碼
能夠很方便的使用 Proxy 來實現一個數據綁定和監聽
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);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
複製代碼
由於 JS 採用 IEEE 754 雙精度版本(64位),而且只要採用 IEEE 754 的語言都有該問題。
咱們都知道計算機表示十進制是採用二進制表示的,因此 0.1
在二進制表示爲
// (0011) 表示循環
0.1 = 2^-4 * 1.10011(0011)
複製代碼
那麼如何獲得這個二進制的呢,咱們能夠來演算下
小數算二進制和整數不一樣。乘法計算時,只計算小數位,整數位用做每一位的二進制,而且獲得的第一位爲最高位。因此咱們得出 0.1 = 2^-4 * 1.10011(0011)
,那麼 0.2
的演算也基本如上所示,只須要去掉第一步乘法,因此得出 0.2 = 2^-3 * 1.10011(0011)
。
回來繼續說 IEEE 754 雙精度。六十四位中符號位佔一位,整數位佔十一位,其他五十二位都爲小數位。由於 0.1
和 0.2
都是無限循環的二進制了,因此在小數位末尾處須要判斷是否進位(就和十進制的四捨五入同樣)。
因此 2^-4 * 1.10011...001
進位後就變成了 2^-4 * 1.10011(0011 * 12次)010
。那麼把這兩個二進制加起來會得出 2^-2 * 1.0011(0011 * 11次)0100
, 這個值算成十進制就是 0.30000000000000004
下面說一下原生解決辦法,以下代碼所示
parseFloat((0.1 + 0.2).toFixed(10))
複製代碼
元字符 | 做用 |
---|---|
. | 匹配任意字符除了換行符和回車符 |
[] | 匹配方括號內的任意字符。好比 [0-9] 就能夠用來匹配任意數字 |
^ | ^9,這樣使用表明匹配以 9 開頭。[^ 9],這樣使用表明不匹配方括號內除了 9 的字符 |
{1, 2} | 匹配 1 到 2 位字符 |
(yck) | 只匹配和 yck 相同字符串 |
| | 匹配 | 先後任意字符 |
\ | 轉義 |
* | 只匹配出現 0 次及以上 * 前的字符 |
+ | 只匹配出現 1 次及以上 + 前的字符 |
? | ? 以前字符可選 |
修飾語 | 做用 |
---|---|
i | 忽略大小寫 |
g | 全局搜索 |
m | 多行 |
簡寫 | 做用 |
---|---|
\w | 匹配字母數字或下劃線 |
\W | 和上面相反 |
\s | 匹配任意的空白符 |
\S | 和上面相反 |
\d | 匹配數字 |
\D | 和上面相反 |
\b | 匹配單詞的開始或結束 |
\B | 和上面相反 |
V8 實現了準確式 GC,GC 算法採用了分代式垃圾回收機制。所以,V8 將內存(堆)分爲新生代和老生代兩部分。
新生代中的對象通常存活時間較短,使用 Scavenge GC 算法。
在新生代空間中,內存空間分爲兩部分,分別爲 From 空間和 To 空間。在這兩個空間中,一定有一個空間是使用的,另外一個空間是空閒的。新分配的對象會被放入 From 空間中,當 From 空間被佔滿時,新生代 GC 就會啓動了。算法會檢查 From 空間中存活的對象並複製到 To 空間中,若是有失活的對象就會銷燬。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。
老生代中的對象通常存活時間較長且數量也多,使用了兩個算法,分別是標記清除算法和標記壓縮算法。
在講算法前,先來講下什麼狀況下對象會出如今老生代空間中:
老生代中的空間很複雜,有以下幾個空間
enum AllocationSpace {
// TODO(v8:7464): Actually map this space's memory as read-only.
RO_SPACE, // 不變的對象空間
NEW_SPACE, // 新生代用於 GC 複製算法的空間
OLD_SPACE, // 老生代常駐對象空間
CODE_SPACE, // 老生代代碼對象空間
MAP_SPACE, // 老生代 map 對象
LO_SPACE, // 老生代大空間對象
NEW_LO_SPACE, // 新生代大空間對象
FIRST_SPACE = RO_SPACE,
LAST_SPACE = NEW_LO_SPACE,
FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
複製代碼
在老生代中,如下狀況會先啓動標記清除算法:
在這個階段中,會遍歷堆中全部的對象,而後標記活的對象,在標記完成後,銷燬全部沒有被標記的對象。在標記大型對內存時,可能須要幾百毫秒才能完成一次標記。這就會致使一些性能上的問題。爲了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標誌。在增量標記期間,GC 將標記工做分解爲更小的模塊,可讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓狀況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名爲併發標記。該技術可讓 GC 掃描和標記對象時,同時容許 JS 運行,你能夠點擊 該博客 詳細閱讀。
清除對象後會形成堆內存出現碎片的狀況,當碎片超過必定限制後會啓動壓縮算法。在壓縮過程當中,將活的對象像一端移動,直到全部對象都移動完成而後清理掉不須要的內存。