JS
中分爲七種內置類型,七種內置類型又分爲兩大類型:基本類型和對象(Object
)。null
,undefined
,boolea
n,number
,string
,symbol
。JS
的數字類型是浮點類型的,沒有整型。而且浮點類型基於 IEEE 754
標準實現,在使用中會遇到某些 Bug。NaN
也屬於 number
類型,而且 NaN
不等於自身。let a = 111 // 這只是字面量,不是 number 類型 a.toString() // 使用時候纔會轉換爲對象類型
對象(
Object
)是引用類型,在使用過程當中會遇到淺拷貝和深拷貝的問題。javascript
let a = { name: 'FE' } let b = a b.name = 'EF' console.log(a.name) // EF
typeof
對於基本類型,除了null
均可以顯示正確的類型css
typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 沒有聲明,可是還會顯示 undefined
typeof
對於對象,除了函數都會顯示object
html
typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function'
對於
null
來講,雖然它是基本類型,可是會顯示object
,這是一個存在好久了的Bug
前端
typeof null // 'object'
PS:爲何會出現這種狀況呢?由於在
JS
的最第一版本中,使用的是32
位系統,爲了性能考慮使用低位存儲了變量的類型信息,000
開頭表明是對象,然而null
表示爲全零,因此將它錯誤的判斷爲object
。雖然如今的內部類型判斷代碼已經改變了,可是對於這個Bug
倒是一直流傳下來。vue
Object.prototype.toString.call(xx)
。這樣咱們就能夠得到相似 [object Type]
的字符串let a // 咱們也能夠這樣判斷 undefined a === undefined // 可是 undefined 不是保留字,可以在低版本瀏覽器被賦值 let undefined = 1 // 這樣判斷就會出錯 // 因此能夠用下面的方式來判斷,而且代碼量更少 // 由於 void 後面隨便跟上一個組成表達式 // 返回就是 undefined a === void 0
轉Booleanjava
在條件判斷時,除了
undefined
,null
,false
,NaN
,''
,0
,-0
,其餘全部值都轉爲true
,包括全部對象node
對象轉基本類型react
對象在轉換基本類型時,首先會調用
valueOf
而後調用toString
。而且這兩個方法你是能夠重寫的jquery
let a = { valueOf() { return 0 } }
四則運算符webpack
只有當加法運算時,其中一方是字符串類型,就會把另外一個也轉爲字符串類型。其餘運算只要其中一方是數字,那麼另外一方就轉爲數字。而且加法運算會觸發三種類型轉換:將值轉換爲原始值,轉換爲數字,轉換爲字符串
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
== 操做符
這裏來解析一道題目
[] == ![] // -> 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__
將對象鏈接起來組成了原型鏈this
在調用 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 }
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__ } }
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
function a() { return () => { return () => { console.log(this) } } } console.log(a()()())
箭頭函數實際上是沒有
this
的,這個函數中的this
只取決於他外面的第一個不是箭頭函數的函數的this
。在這個例子中,由於調用a
符合前面代碼中的第一個狀況,因此this
是window
。而且 this 一旦綁定了上下文,就不會被任何代碼改變
當執行 JS 代碼時,會產生三種執行上下文
eval
執行上下文每一個執行上下文中都有三個重要的屬性
VO
),包含變量、函數聲明和函數的形參,該屬性只能在全局上下文中訪問JS
採用詞法做用域,也就是說變量的做用域是在定義時就決定了)this
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( i ); }, i*1000 ); } i++ { let ii = i } i++ { let ii = i } ... }
letet a 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
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
的時候,該對象也不能正常的序列化let a = { age: undefined, jobs: function() {}, name: 'poetries' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "poetries"}
lodash
的深拷貝函數。在有
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
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
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() })
你是否在平常開發中遇到一個問題,在滾動事件中須要作個複雜計算或者實現一個按鈕的防二次點擊操做。
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
,就能夠再次點擊了。ID
,若是是延遲調用就調用函數防抖動和節流本質是不同的。防抖動是將屢次執行變爲最後一次執行,節流是將屢次執行變成每隔一段時間執行
/** * 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'])
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);