夯實JS主要知識點

從事前端行業到如今,感受本身進步最大的時候就是去年打算換工做開始學習的那段時間,特別是看 yck 大佬的掘金小冊《前端面試之道》的那段時間。正是那段時間的學習,慢慢對前端知識體系有了個模糊的輪廓,並且也開始接觸掘金這個有意思的技術平臺。現在工做塵埃落定,倒開始懶散了,通勤路上又開始玩遊戲了,晚上回家又開始玩遊戲不看書了,閒的時候開始在微信羣QQ羣注水了。但是距離30歲愈來愈近,眼前的路卻愈來愈模糊。我知道留給我補課的時間很少了。工做的前三年已經被我揮霍掉,若是這兩年不把失去的時間補回來,我可能永遠都只能停留在初中級程序員的水平。謹記我仍是一個半路出家的非科班出身的大齡初級前端開發工程師,自勉!javascript

小剛老師

基本類型和引用類型

js中數據類型分爲基本類型和引用類型,基本類型有六種:html

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol (es6)

引用類型包括對象object、數組array、函數function等,統稱對象類型:前端

  • object

string類型即字符串,除了單引號雙引號,es6 中引入了新的反引號 ` ` 來包含字符串。反引號的擴展功能是能夠用${…}將變量和表達式嵌入到字符串中。使用以下:java

let n = 3
let m = () => 4
let str = `m + n = ${m() + n}` // "m + n = 7"
複製代碼

number類型值包括整數、浮點數、NaNInfinity等。其中NaN類型是js中惟一不等於自身的類型,當發生未定義的數學操做的時候,就會返回NaN,如:1 * 'asdf'Number('asdf')。浮點數的運算可能會出現如0.1 + 0.2 !== 0.3的問題,這是因爲浮點運算的精度的問題,通常採用toFixed(10)即可以解決此類問題。程序員

booleanstringnumber類型做爲基本類型,按理說應該是沒有函數能夠調用的,由於基本類型沒有原型鏈能夠提供方法。可是,這三種類型卻能調用toString等對象原型上的方法。不信?es6

true.toString() // 'true'
`asdf`.toString() // 'asdf'
NaN.toString() // 'NaN'
複製代碼

你可能會說,那爲何數字1不能調用toString方法呢?其實,不是不能調用:面試

1 .toString()
1..toString()
(1).toString()
複製代碼

以上三種調用都是能夠的,數字後面的第一個點會被解釋爲小數點,而不是點調用。只不過不推薦這種使用方法,並且這樣作也沒什麼意義。數組

爲何基本類型卻能夠直接調用引用類型的方法呢?實際上是js引擎在解析上面的語句的時候,會把這三種基本類型解析爲包裝對象(就是下面的new String()),而包裝對象是引用類型能夠調用Object.prototype上的方法。大概過程以下:瀏覽器

'asdf'.toString()  ->  new String('asdf').toString()  -> 'asdf'
複製代碼

null含義爲「無」、「空」或「值未知」的特殊值。bash

undefined的含義是「未被賦值」。除了變量已聲明未賦值的狀況下是undefined,若對象的屬性不存在也是undefined。因此應該儘可能避免使用var a = undefined; var o = {b: undefined}這樣的寫法,取而代之用var a = null; var o = {b: null},以與「未被賦值」默認undefined的狀況相區分。

Symbol值表示惟一的標識符。能夠用Symbol()函數建立:

var a = Symbol('asdf')
var b = Symbol('asdf')
a === b // false
複製代碼

還能夠建立全局標識符,這樣能夠在訪問相同的名稱的時候都獲得同一個標識符。以下:

var a = Symbol.for('asdf')
var b = Symbol.for('asdf')
a === b // true
複製代碼

還能夠用作對象的屬性,但此時是不能被for...in遍歷的:

let id = Symbol('id')
let obj = {
  [id]: 'ksadf2sdf3lsdflsdjf090sld',
  a: 'a',
  b: 'b'
}
for(let key in obj){ console.log(key) } // a b
obj[id] // "ksadf2sdf3lsdflsdjf090sld"
複製代碼

還存在不少系統內置的Symbol,如Symbol.toPrimitive Symbol.iterator 等。當發生引用類型強制轉基本類型的操做時,就會觸發內置的Symbol.toPrimitive函數,固然也能夠給對象手動添加Symbol.toPrimitive函數來覆蓋默認的強制類型轉換行爲。

object是引用類型,引用類型和基本類型不一樣的是,原始類型存儲的是值,引用類型存儲的是一個指向對象真實內存地址的指針。在 js 中,對象包括Array Object Function RegExp Math等。

js 全部的函數語句都是在執行棧中執行的,全部的變量也在執行棧中保存着值或引用。基本類型就存儲在棧內存中,保存的是實際值;引用類型存儲在堆內存中,在棧中只保存着變量指向內存地址的指針。

var o = {
  a: 'a',
  b: 'b'
}
var o2 = o // 變量o2複製了變量o的指針,如今他們都指向同一個內存地址,如今開始他們的增刪改實際上是在同一個內存地址上的操做
o2.c = 'c' // (增)如今o.c也是'c'
delete o2.b // (刪)如今o.b也不存在了
o2.a = 'a2' // (改)如今o.a也是'a2'
o2 = 'o2' // 如今變量o2被賦值'o2',已經和原來的內存地址斷絕了關係,但變量 o 仍然指向老地址
複製代碼

類型判斷

判斷引用類型和基本類型的類型是不一樣的,判斷基本類型能夠用typeof

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
複製代碼

能夠看到除了null其餘基本類型的判斷都是正常的,typeof(null) === 'object'是一個歷史悠久的 bug,就是在 JS 的最第一版本中null的內存存儲信息是000開頭的,而000開頭的會被判斷爲object類型。雖然如今內部類型判斷代碼已經改變了,可是這個 bug 卻不得不隨着版本保留了下來,由於修改這個 bug 會致使巨多的網站出現 bug 。

typeof對引用類型,除了函數返回function,其餘都返回object。但咱們開發中數組確定是要返回array類型的,因此typeof對引用類型來講並非很適用。判斷引用類型通常用instanceof

var obj = {}
var arr = []
var fun = () => {}
typeof obj // 'object'
typeof arr // 'object'
typeof fun // 'function'
obj instanceof Object // true
arr instanceof Array // true
fun instanceof Function // true
複製代碼

能夠看到instanceof操做符能夠正確判斷出引用類型的類型。instanceof本質上是判斷右邊的構造函數的prototype對象是否存在於左邊的原型鏈上,是的話返回true。因此不論數組、對象仍是函數,... instanceof Object都返回true

最後來一種全能型判斷類型方法:Object.prototype.toString.call(...),能夠自行嘗試。

強制類型轉換

JS 是弱類型語言,不一樣類型之間在必定狀況下會發生強制類型轉換,好比在相等性比較的時候。

基本類型的相等性比較的是值是否同樣,對象相等性比較的是內存地址是否相同。下面來看一個有意思的比較把:

[] == [] // ?
[] == ![] // ?
複製代碼

對於[] {} function (){}這樣的沒有被賦值給變量的引用類型來講,他們只在當前語句中有效,並且不相等於其餘任何對象。由於根本沒法找到他們的內存地址的指針。因此[] == []false

對於[] == ![],由於涉及到強制類型轉換,因此複雜的多了。想要更加詳細瞭解強制類型轉換能夠看我這篇文章

在 JS 中類型轉換隻有三種狀況:toNumbertoStringtoBoolean 。正常狀況下轉換規則以下:

原始值/類型 目標類型:number 結果
null number 0
symbol number 拋錯
string number '1'=>1 '1a'=>NaN ,含非數字則爲NaN
數組 number []=>0 ['1']=>1 ['1', '2']=>NaN
object/function/undefined number NaN
原始值/類型 目標類型:string 結果
number string 1=>'1'
array string [1, 2]=>'1,2'
布爾值/函數/symbol string 原始值加上引號,如:'true' 'Sumbol()'
object string {}=>'[object Object]'
原始值/類型 目標類型:boolean 結果
number boolean 除了0NaNfalse,其餘都是true
string boolean 除了空字符串爲false,其餘都爲true
null/undefined boolean false
引用類型 boolean true

如今來揭開 [] == ![] 返回true的真相把:

[] == ![] // true
/* * 首先,布爾操做符!優先級更高,因此被轉變爲:[] == false * 其次,操做數存在布爾值false,將布爾值轉爲數字:[] == 0 * 再次,操做數[]是對象,轉爲原始類型(先調用valueOf(),獲得的仍是[],再調用toString(),獲得空字符串''):'' == 0 * 最後,字符串和數字比較,轉爲數字:0 == 0 */
NaN == NaN // false NaN不等於任何值
null == undefined // true
null == 0 // false
undefined == 0 // false
複製代碼

做用域

js 中的做用域是詞法做用域,是由 函數聲明時 所在的位置決定的。詞法做用域是指在編譯階段就產生的,一整套函數標識符的訪問規則。 說到底js的做用域只是一個「空地盤」,其中並無真實的變量,可是卻定義了變量如何訪問的規則。(詞法做用域是在編譯階段就確認的,區別於詞法做用域,動態做用域是在函數執行的時候確認的,js的沒有動態做用域,但js的this很像動態做用域,後面會提到。語言也分爲靜態語言和動態語言,靜態語言是指數據類型在編譯階段就肯定的語言如 java,動態語言是指在運行階段才肯定數據類型的語言如 javascript。)

做用域鏈本質上是一個指向變量對象的指針列表,它只引用不包含實際變量對象,是做用域概念的延申。做用域鏈定義了在當前上下文訪問不到變量的時候如何沿做用域鏈繼續查詢變量的一套規則。

event loop

js 是單線程的,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。可是IO設備(輸入輸出設備)很慢(好比Ajax操做從網絡讀取數據),js 不可能等待IO設備執行完成才繼續執行下一個的任務,這樣就失去了這門語言的意義。因此 js 的任務分爲同步任務和異步任務。

  1. 全部同步任務都是在主線程執行,造成一個「執行棧」(就是下圖中的stack);
  2. 全部的異步任務都會暫時掛起,等待運行有告終果以後,其回調函數就會進入「任務隊列」(task queue)排隊等待;
  3. 當執行棧中的全部同步任務都執行完成以後,就會讀取任務隊列中的第一個的回調函數,並將該回調函數推入執行棧開始執行;
  4. 主線程不斷循環重複第三步,這就是「event loop」的運行機制。

上圖中,主線程運行的時候,產生堆(heap)和棧(stack),堆用來存放數組對象等引用類型,棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。

任務隊列中有兩種異步任務,一種是宏任務,包括script setTimeout setInterval等,另外一種是微任務,包括Promise process.nextTick MutationObserver等。每當一個 js 腳本運行的時候,都會先執行script中的總體代碼;當執行棧中的同步任務執行完畢,就會執行微任務中的第一個任務並推入執行棧執行,當執行棧爲空,則再次讀取執行微任務,循環重複直到微任務列表爲空。等到微任務列表爲空,纔會讀取宏任務中的第一個任務並推入執行棧執行,當執行棧爲空則再讀取執行微任務,微任務爲空纔再讀取執行宏任務,如此循環。

執行上下文:

執行上下文是指 函數調用時 在執行棧中產生的當前函數(或全局對象window)的執行環境,這個環境就像一個隔絕外面世界的容器結界,裏面存放着能夠訪問的變量、this對象等。例如:

let fn, bar; // 一、進入全局上下文環境
bar = function(x) {
  let b = 5;
  fn(x + b); // 三、進入fn函數上下文環境
};
fn = function(y) {
  let c = 5;
  console.log(y + c); //四、fn出棧,bar出棧
};
bar(10); // 二、進入bar函數上下文環境
複製代碼

每次函數調用時,執行棧棧頂都會產生一個新的執行上下文環境。棧底永遠都是全局上下文,而棧頂就是當前處於活動狀態的正在執行代碼的上下文。

理解函數的執行過程

本文重點,讓你對函數執行過程的理解更上一層樓!

函數的執行過程分紅兩階段,第一階段是建立執行上下文環境階段,第二階段是代碼執行階段:

  • 建立執行上下文階段(發生在 函數被調用時 && 函數體內的代碼執行前 )。

  1. 建立變量對象,這個過程會:建立 arguments 對象,初始化函數參數變量 ---> 檢查建立當前上下文環境中的function函數聲明(即所謂的函數聲明提高) ---> 檢查建立當前上下文環境中的var變量聲明(即所謂變量提高)、let const聲明;

  1. 創建做用域鏈,肯定在當前上下文環境尋找變量的規則;
  2. 肯定this對象的指向;
  • 代碼執行階段
  1. 執行函數體內的代碼,這個階段會完成變量賦值,函數引用,以及執行其餘代碼。

在未進入執行階段以前,變量對象中的屬性還在建立都不能訪問。可是進入執行階段以後,變量對象建立完成轉變爲了活動對象,裏面的屬性都能被訪問了,而後纔開始進行執行階段的操做。也就是說,變量對象和活動對象的惟一區別就是處於執行上下文的不一樣生命週期。

變量對象更詳細介紹參考此文

this 指向

let fn = function(){
  alert(this.name)
}
let obj = {
  name: '',
  fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
複製代碼
  1. 方法1中直接調用函數fn(),這種看着像光桿司令的調用方式,this指向window(嚴格模式下是undefined)。
  2. 方法2中是點調用obj.fn(),此時this指向obj對象。點調用中this指的是點前面的對象。
  3. 方法3中利用call函數把fn中的this指向了第一個參數,這裏是obj。即利用callapplybind函數能夠把函數的this變量指向第一個參數。
  4. 方法4中用new實例化了一個對象instance,這時fn中的this就指向了實例instance

若是同時發生了多個規則怎麼辦?其實上面四條規則的優先級是遞增的:

fn() < obj.fn() < fn.call(obj) < new fn()

首先,new調用的優先級最高,只要有new關鍵字,this就指向實例自己;接下來若是沒有new關鍵字,有call、apply、bind函數,那麼this就指向第一個參數;而後若是沒有new、call、apply、bind,只有obj.foo()這種點調用方式,this指向點前面的對象;最後是光桿司令foo() 這種調用方式,this指向window(嚴格模式下是undefined)。

es6中新增了箭頭函數,而箭頭函數最大的特點就是沒有本身的this、arguments、super、new.target,而且箭頭函數沒有原型對象prototype不能用做構造函數(new一個箭頭函數會報錯)。由於沒有本身的this,因此箭頭函數中的this其實指的是包含函數中的this。不管是點調用,仍是call調用,都沒法改變箭頭函數中的this

閉包

很長時間以來我對閉包都停留在「定義在一個函數內部的函數」這樣膚淺的理解上。事實上這只是閉包造成的必要條件之一。直到後來看了kyle大佬的《你不知道的javascript》上冊關於閉包的定義,我才豁然開朗:

當函數可以記住並訪問所在的詞法做用域時,就產生了閉包。

let single = (function(){
  let count = 0
  return {
    plus(){
      count++
      return count
    },
    minus(){
      count--
      return count
    }
  }
})()
single.plus() // 1
single.minus() // 0
複製代碼

這是個單例模式,這個模式返回了一個對象並賦值給變量single,變量single中包含兩個函數plusminus,而這兩個函數都用到了所在詞法做用域中的變量count。正常狀況下count和所在的執行上下文會在函數執行結束時被銷燬,可是因爲count還在被外部環境使用,因此在函數執行結束時count和所在的執行上下文不會被銷燬,這就產生了閉包。每次調用single.plus()或者single.minus(),就會對閉包中的count變量進行修改,這兩個函數就保持住了對所在的詞法做用域的引用。

閉包實際上是一種特殊的函數,它能夠訪問函數內部的變量,還可讓這些變量的值始終保持在內存中,不會在函數調用後被垃圾回收機制清除。

看個經典安利:

// 方法1
for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
複製代碼

方法1中,循環設置了五個定時器,一秒後定時器中回調函數將執行,打印變量i的值。毋庸置疑,一秒以後i已經遞增到了6,因此定時器打印了五次6 。(定時器中並無找到當前做用域的變量i,因此沿做用域鏈找到了全局做用域中的i

方法2中,因爲es6的let會建立局部做用域,因此循環設置了五個做用域,而五個做用域中的變量i分佈是1-5,每一個做用域中又設置了一個定時器,打印一秒後變量i的值。一秒後,定時器從各自父做用域中分別找到的變量i是1-5 。這是個利用閉包解決循環中變量發生異常的新方法。

原型和原型鏈

js 中的幾乎全部對象都有一個特殊的[[Prototype]]內置屬性,用來指定對象的原型對象,這個屬性實質上是對其餘對象的引用。在瀏覽器中通常都會暴露一個私有屬性 __proto__,其實就是[[Prototype]]的瀏覽器實現。假若有一個對象var obj = {},那麼能夠經過obj.__proto__ 訪問到其原型對象Object.prototype,即obj.__proto__ === Object.prototype。對象有[[Prototype]]指向一個原型對象,原型對象自己也是對象也有本身的[[Prototype]]指向別的原型對象,這樣串接起來,就組成了原型鏈。

var obj = [1, 2, 3]
obj.__proto__ === Array.prototype // true
Number.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
obj.toString()
複製代碼

能夠看出,上例中存在一個從objnull的原型鏈,以下:

obj----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null
複製代碼

上例中最後一行調用obj.toString()方法的時候,js 引擎就是沿着這條原型鏈查找toString方法的。js 首先在obj對象自身上查找toString方法;未找到,繼續沿着原型鏈查找Array.prototype上有沒有toString;未找到,繼續沿着原型鏈在Object.prototype上查找。最終在Object.prototype上找到了toString方法,因而淚流滿面的調用該方法。這就是原型鏈最基本的做用。原型鏈仍是 js 實現繼承的本質所在,下一小節再講。

上面我說「js 中的幾乎全部對象都有一個特殊的[[Prototype]]內置屬性」,爲何不是所有呢?由於 js 能夠建立沒有內置屬性[[Prototype]]的對象:

var o = Object.create(null)
o.__proto__ // undefined
複製代碼

Object.create是 es5 的方法,全部瀏覽器都已支持。該方法建立並返回一個新對象,並將新對象的原型對象賦值爲第一個參數。在上例中,Object.create(null)建立了一個新對象並將對象的原型對象賦值爲null。此時對象 o 是沒有內置屬性[[Prototype]]的(不知道爲何o.__proto__不是null,但願知道的大佬評論解釋下,萬分感激)。

js 的繼承

js 的繼承是經過原型鏈實現的,具體能夠參考個人這篇文章,這裏我只講一講你們可能比較陌生的「行爲委託」。行爲委託是《你不知道的JavaScript》系列做者 kyle 大佬推薦的一種代替繼承的方式,該模式主要利用setPrototypeOf方法把一個對象的內置原型[[Protytype]]關聯到另外一個對象上,從而達到繼承的目的。

let SuperType = {
  initSuper(name) {
    this.name = name
    this.color = [1,2,3]
  },
  sayName() {
    alert(this.name)
  }
}
let SubType = {
  initSub(age) {
    this.age = age
  },
  sayAge() {
    alert(this.age)
  }
}
Object.setPrototypeOf(SubType,SuperType)
SubType.initSub('17')
SubType.initSuper('gim')
SubType.sayAge() // '17'
SubType.sayName() // 'gim'
複製代碼

上例就是把父對象SuperType關聯到子對象SubType的內置原型上,這樣就能夠在子對象上直接調用父對象上的方法。行爲委託生成的原型鏈比class繼承生成的原型鏈的關係簡單清晰,一目瞭然。

kyle大佬倡導的行爲委託
相關文章
相關標籤/搜索