JavaScript三大重點(diǎn)同步異步與作用域和閉包及原型和原型鏈詳解

如圖所示,JS的三座大山:
- 同步、異步
- 作用域、閉包
- 原型、原型鏈
1. 同步、異步
JavaScript執(zhí)行機(jī)制,重點(diǎn)有兩點(diǎn):
- JavaScript是一門單線程語言
- Event Loop(事件循環(huán))是JavaScript的執(zhí)行機(jī)制
JS為什么是單線程
最初設(shè)計(jì)JS是用來在瀏覽器驗(yàn)證表單操控DOM元素的是一門腳本語言,如果js是多線程的,那么兩個線程同時對一個DOM元素進(jìn)行了相互沖突的操作,那么瀏覽器的解析器是無法執(zhí)行的。
js為什么需要異步
如果js中不存在異步,只能自上而下執(zhí)行,如果上一行解析時間很長,那么下面的代碼就會被阻塞。
對于用戶而言,阻塞就以為著“卡死”,這樣就導(dǎo)致了很差的用戶體驗(yàn)。比如在進(jìn)行ajax請求的時候如果沒有返回數(shù)據(jù)后面的代碼就沒辦法執(zhí)行
JS的事件循環(huán)(eventloop)是怎么運(yùn)作的
- 首先判斷JS是同步還是異步,同步就進(jìn)入主線程運(yùn)行,異步就進(jìn)入event table.
- 異步任務(wù)在event table中注冊事件,當(dāng)滿足觸發(fā)條件后,(觸發(fā)條件可能是延時也可能是ajax回調(diào)),被推入event queue
- 同步任務(wù)進(jìn)入主線程后一直執(zhí)行,直到主線程空閑時,才會去event queue中查看是否有可執(zhí)行的異步任務(wù),如果有就推入主線程中。
如圖所示:

那怎么知道主線程執(zhí)行棧為空呢?js引擎存在monitoring process進(jìn)程,會持續(xù)不斷的檢查 主線程執(zhí)行棧是否為空,一旦為空,就會去event queue那里檢查是否有等待被調(diào)用的函數(shù)
宏任務(wù) 包含整個script代碼塊,setTimeout, setIntval
微任務(wù) Promise , process.nextTick
在劃分宏任務(wù)、微任務(wù)的時候并沒有提到async/ await的本質(zhì)就是Promise
setTimeout(function() {
console.log('4')
})
new Promise(function(resolve) {
console.log('1') // 同步任務(wù)
resolve()
}).then(function() {
console.log('3')
})
console.log('2')
執(zhí)行結(jié)果: 1-2-3-4
1. 這段代碼作為宏任務(wù),進(jìn)入主線程。
2. 先遇到setTimeout,那么將其回調(diào)函數(shù)注冊后分發(fā)到宏任務(wù)event queue.
3. 接下來遇到Promise, new Promise立即執(zhí)行,then函數(shù)分發(fā)到微任務(wù)event queue
4. 遇到console.log(), 立即執(zhí)行
5. 整體代碼script作為第一個宏任務(wù)執(zhí)行結(jié)束, 查看當(dāng)前有沒有可執(zhí)行的微任務(wù),執(zhí)行then的回調(diào)。(第一輪事件循環(huán)結(jié)束了,我們開始第二輪循環(huán))
6. 從宏任務(wù)的event queue開始,我們發(fā)現(xiàn)了宏任務(wù)event queue中setTimeout對應(yīng)的回調(diào)函數(shù),立即執(zhí)行。
console.log('1')
setTimeout(function() {
console.log('2')
process.nextTick(function() {
console.log('3')
})
new Promise(function(resolve) {
console.log('4')
resolve()
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6')
})
new Promise(function(resolve) {
console.log('7')
resolve()
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9')
process.nextTick(function() {
console.log('10')
})
new Promise(function(resolve) {
console.log('11')
resolve()
}).then(function() {
console.log('12')
})
})1.整體script作為第一個宏任務(wù)進(jìn)入主線程,遇到console.log(1)輸出1
遇到setTimeout, 其回調(diào)函數(shù)被分發(fā)到宏任務(wù)event queue中。我們暫且記為setTimeout1
3.遇到process.nextTick(),其回調(diào)函數(shù)被分發(fā)到微任務(wù)event queue中,我們記為process1
4.遇到Promise, new Promise直接執(zhí)行,輸出7.then被分發(fā)到微任務(wù)event queue中,我們記為then1
又遇到setTimeout,其回調(diào)函數(shù)被分發(fā)到宏任務(wù)event queue中,我們記為setTimeout2.
現(xiàn)在開始執(zhí)行微任務(wù), 我們發(fā)現(xiàn)了process1和then1兩個微任務(wù),執(zhí)行process1,輸出6,執(zhí)行then1,輸出8, 第一輪事件循環(huán)正式結(jié)束, 這一輪的結(jié)果輸出1,7,6,8.那么第二輪事件循環(huán)從setTimeout1宏任務(wù)開始
5. 首先輸出2, 接下來遇到了process.nextTick(),統(tǒng)一被分發(fā)到微任務(wù)event queue,記為process2
8new Promise立即執(zhí)行,輸出4,then也被分發(fā)到微任務(wù)event queue中,記為then2
6. 現(xiàn)在開始執(zhí)行微任務(wù),我們發(fā)現(xiàn)有process2和then2兩個微任務(wù)可以執(zhí)行輸出3,5. 第二輪事件循環(huán)結(jié)束,第二輪輸出2,4,3,5. 第三輪事件循環(huán)從setTimeout2哄任務(wù)開始
10。 直接輸出9,跟第二輪事件循環(huán)類似,輸出9,11,10,12
7. 完整輸出是1,7,6,8,2,4,3,5,9,11,10,12(請注意,node環(huán)境下的事件監(jiān)聽依賴libuv與前端環(huán)境不完全相同,輸出順序可能會有誤差)
async/await用來干什么
用來優(yōu)化promise的回調(diào)問題,被稱為是異步的終極解決方案
async/await內(nèi)部做了什么
async函數(shù)會返回一個Promise對象,如果在函數(shù)中return一個直接量(普通變量),async會把這個直接量通過Promise.resolve()封裝成Promise對象。如果你返回了promise那就以你返回的promise為準(zhǔn)。await是在等待,等待運(yùn)行的結(jié)果也就是返回值。await后面通常是一個異步操作(promise),但是這不代表await后面只能跟異步才做,await后面實(shí)際是可以接普通函數(shù)調(diào)用或者直接量。
async相當(dāng)于 new Promise,await相當(dāng)于then
await的等待機(jī)制
如果await后面跟的不是一個promise,那await后面表達(dá)式的運(yùn)算結(jié)果就是它等到的東西,如果await后面跟的是一個promise對象,await它會’阻塞’后面的diamante,等著promise對象resolve,
然后得到resolve的值作為await表達(dá)式的運(yùn)算結(jié)果。但是此"阻塞"非彼“阻塞”,這就是await必須用在async函數(shù)中的原因。 async函數(shù)調(diào)用不會造成"阻塞",它內(nèi)部所有的“阻塞”都被封裝在一個promise對象中異步執(zhí)行(這里的阻塞理解成異步等待更合理)
async/await在使用過程中有什么規(guī)定
每個async方法都返回一個promise, await只能出現(xiàn)在async函數(shù)中
async/await在什么場景使用
單一的promise鏈并不能發(fā)現(xiàn)async/await的有事,但是如果需要處理由多個promise組成的then鏈的時候,優(yōu)勢就能體現(xiàn)出來了(Promise通過then鏈來解決多層回調(diào)的問題,現(xiàn)在又用async/awai來進(jìn)一步優(yōu)化它)
2. 作用域、閉包
閉包
- 閉包是指有權(quán)訪問另外一個函數(shù)作用域中的變量的函數(shù)(紅寶書)
- 閉包是指那些能夠訪問自由變量的函數(shù)。(MDN)其中自由變量, 指在函數(shù)中使用的,但既不是函數(shù)參數(shù)arguments也不是函數(shù)的局部變量的變量,其實(shí)就是另外一個函數(shù)作用域中的變量。)
作用域
說起閉包,就必須要說說作用域,ES5種只存在兩種作用域:
- 函數(shù)作用域。
- 全局作用域 當(dāng)訪問一個變量時,解釋器會首先在當(dāng)前作用域查找標(biāo)示符,如果沒有找到, 就去父作用域找, 直到找到該變量的標(biāo)示符或者不在父作用域中
- 這就是作用域鏈,每一個子函數(shù)都會拷貝上級的作用域, 形成一個作用域的鏈條。
let a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a); //3
}
}在這段代碼中,
- f1的作用域指向有全局作用域(window) 和它本身
- 而f2的作用域指向全局作用域(window)、 f1和它本身
- 而且作用域是從最底層向上找, 直到找到全局作用域window為止
- 如果全局還沒有的話就會報錯。閉包產(chǎn)生的本質(zhì)就是
- 當(dāng)前環(huán)境中存在指向父級作用域的引用
function f2() {
var a = 2
function f3() {
console.log(a); //2
}
return f3;
}
var x = f2();
x();這里x會拿到父級作用域中的變量, 輸出2。
因?yàn)樵诋?dāng)前環(huán)境中,含有對f3的引用, f3恰恰引用了window、 f3和f3的作用域。
因此f3可以訪問到f2的作用域的變量。那是不是只有返回函數(shù)才算是產(chǎn)生了閉包呢?回到閉包的本質(zhì),只需要讓父級作用域的引用存在即可。
var f4;
function f5() {
var a = 2
f4 = function () {
console.log(a);
}
}
f5();
f4();讓f5執(zhí)行,給f4賦值后,等于說現(xiàn)在f4擁有了window、f5和f4本身這幾個作用域的訪問權(quán),還是自底向上查找,最近是在f5中找到了a,因此輸出2。在這里是外面的變量f4存在著父級作用域的引用,
因此產(chǎn)生了閉包,形式變了,本質(zhì)沒有改變。
場景
- 返回一個函數(shù)
- 作為函數(shù)參數(shù)傳遞
- 在定時器、 事件監(jiān)聽、 Ajax請求、 跨窗口通信、 Web Workers或者任何異步中,只要使用了回調(diào)函數(shù), 實(shí)際上就是在使用閉包。
IIFE(立即執(zhí)行函數(shù)表達(dá)式) 創(chuàng)建閉包, 保存了全局作用域window和當(dāng)前函數(shù)的作用域。
var b = 1;
function foo() {
var b = 2;
function baz() {
console.log(b);
}
bar(baz);
}
function bar(fn) {
// 這就是閉包
fn();
}
// 輸出2,而不是1
foo();
// 以下的閉包保存的僅僅是window和當(dāng)前作用域。
// 定時器
setTimeout(function timeHandler() {
console.log('111');
}, 100)
// 事件監(jiān)聽
// document.body.click(function () {
// console.log('DOM Listener');
// })
// 立即執(zhí)行函數(shù)
var c = 2;
(function IIFE() {
// 輸出2
console.log(c);
})();經(jīng)典的一道題
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
} // 6 6 6 6 6 6
// 為什么會全部輸出6? 如何改進(jìn), 讓它輸出1, 2, 3, 4, 5?解析:
- 因?yàn)閟etTimeout為宏任務(wù), 由于JS中單線程eventLoop機(jī)制, 在主線程同步任務(wù)執(zhí)行完后才去執(zhí)行宏任務(wù)。
- 因此循環(huán)結(jié)束后setTimeout中的回調(diào)才依次執(zhí)行, 但輸出i的時候當(dāng)前作用域沒有。
往上一級再找,發(fā)現(xiàn)了i,此時循環(huán)已經(jīng)結(jié)束,i變成了6,因此會全部輸出6。
利用IIFE(立即執(zhí)行函數(shù)表達(dá)式)當(dāng)每次for循環(huán)時,把此時的i變量傳遞到定時器中
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j)
}, 1000);
})(i)
}給定時器傳入第三個參數(shù), 作為timer函數(shù)的第一個函數(shù)參數(shù)
for (var i = 0; i < 5; i++) {
setTimeout(function (j) {
console.log(j)
}, 1000, i);
}使用ES6中的let
- let使JS發(fā)生革命性的變化, 讓JS有函數(shù)作用域變?yōu)榱藟K級作用域
- 用let后作用域鏈不復(fù)存在。 代碼的作用域以塊級為單位,
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 2000)
}3. 原型、原型鏈
原型(prototype)
JS中所有函數(shù)都會有prototype屬性,只有函數(shù)才有
其所有的屬性和方法都能被構(gòu)造函數(shù)的實(shí)例對象共享訪問
代碼如下:
function Person(name){
this.name = name
}
Person.prototype.sayHello(){
console.log('sayHello')
}
let p1 = new Person();
let p2 = new Person();
console.log(p1.sayHello) //sayHello
console.log(p2.sayHello) //sayHello構(gòu)造函數(shù)(constructor)
JS中constructor存在每個函數(shù)的prototype屬性中,其保存了指向該函數(shù)的引用
Person.prototype.constructor ==Person //true
原型鏈(_ _ proto _ _)
JS中對象都會有個內(nèi)置屬性,即__proto__,(隱式原型鏈的屬性),一般情況下執(zhí)行創(chuàng)建它的構(gòu)造函數(shù)的prototype的屬性,另外函數(shù)比較特殊,也會有該屬性
p1.__proto__ == Person.prototype
JS 引擎查找摸個屬性時,先查找對象本身是否存在該屬性,如果不存在就會在原型鏈上一層一層進(jìn)行查找
有幾個面試經(jīng)常會問的幾個問題
如何精確地判斷短數(shù)組的類型
[] instanceof Array //[].__proto__ == Array.prototype Object.prototype.toString.call([]) //[Object Array] Array.isArray([]) //true [].constructor ==Array
下面代碼輸出什么
Object instanceof Function //true Function instanceof Object // true
實(shí)現(xiàn)一個原型鏈繼承
function Person(name){
this.name = name
}
Person.prototype.sayHello(){
console.log('sayHello')
}
function Boy(){};
Boy.prototype = new Person();
let b1 = new Boy();
b1.sayHello() //sayHello原型、原型鏈、構(gòu)造函數(shù)、實(shí)例的關(guān)系

1.instanceof檢測構(gòu)造函數(shù)與實(shí)例的關(guān)系:
function Person () {.........}
person = new Person ()
res = person instanceof Person
res // true2.實(shí)例繼承原型上的定義的屬性:
function Person () {........}
Person.prototype.type = 'object n'
person = new Person ()
res = person.type
res // object n3.實(shí)例訪問 ===> 原型
實(shí)例通過__proto__訪問到原型 person.proto=== Person.prototype
4.原型訪問 ===> 構(gòu)造函數(shù)
原型通過constructor屬性訪問構(gòu)造函數(shù) Person.prototype.constructor === Person
5.實(shí)例訪問===>構(gòu)造函數(shù)
person.proto.constructor === Person
原型鏈
在讀取一個實(shí)例的屬性的過程中,如果屬性在該實(shí)例中沒有找到,那么就會循著 proto 指定的原型上去尋找,如果還找不到,則尋找原型的原型:
實(shí)例上尋找
function Person() {}
Person.prototype.type = "object name Person";
person = new Person();
person.type = "我是實(shí)例的自有屬性";
res = Reflect.ownKeys(person); //嘗試獲取到自有屬性
console.log(res);
res = person.type;
console.log(res); //我是實(shí)例的自有屬性(通過原型鏈向上搜索優(yōu)先搜索實(shí)例里的)原型上尋找
function Person() {}
Person.prototype.type = "object name Person";
person = new Person();
res = Reflect.ownKeys(person); //嘗試獲取到自有屬性
console.log(res);
res = person.type;
console.log(res); //object name Person原型的原型上尋找
function Person() {}
Person.prototype.type = "object name Person";
function Child() {}
Child.prototype = new Person();
p = new Child();
res = [p instanceof Object, p instanceof Person, p instanceof Child];
console.log(res); //[true, true, true] p同時屬于Object,Person, Child
res = p.type; //層層搜索
console.log(res); //object name Person (原型鏈上搜索)
console.dir(Person);
console.dir(Child);原型鏈上搜索
原型同樣也可以通過 proto 訪問到原型的原型,比方說這里有個構(gòu)造函數(shù) Child 然后“繼承”前者的有一個構(gòu)造函數(shù) Person,然后 new Child 得到實(shí)例 p;
當(dāng)訪問 p 中的一個非自有屬性的時候,就會通過 proto 作為橋梁連接起來的一系列原型、原型的原型、原型的原型的原型直到 Object 構(gòu)造函數(shù)為止;
原型鏈搜索搜到 null 為止,搜不到那訪問的這個屬性就停止:
function Person() {}
Person.prototype.type = "object name Person";
function Child() {}
Child.prototype = new Person();
p = new Child();
res = p.__proto__;
console.log(res); //Person {}
res = p.__proto__.__proto__;
console.log(res); //Person {type:'object name Person'}
res = p.__proto__.__proto__.__proto__;
console.log(res); //{.....}
res = p.__proto__.__proto__.__proto__.__proto__;
console.log(res); //null繼承
- JS 中一切皆對象(所有的數(shù)據(jù)類型都可以用對象來表示),必須有一種機(jī)制,把所有的對象聯(lián)系起來,實(shí)現(xiàn)類似的“繼承”機(jī)制。
- 不同于大部分面向?qū)ο笳Z言,ES6 之前并沒有引入類(class)的概念,JS 并非通過類而是通過構(gòu)造函數(shù)來創(chuàng)建實(shí)例,javascript中的繼承是通過原型鏈來體現(xiàn)的。
- 其基本思想是利用原型讓一個引用類型繼承另一個引用繼承的屬性和方法。
什么是繼承
- js中,繼承是一種允許我們在已有類的基礎(chǔ)上創(chuàng)建新類的機(jī)制;它可以使用現(xiàn)有類的所有功能,并在無需重新編寫
- 原來的類的情況下對這些功能進(jìn)行擴(kuò)展。
為什么要有繼承
提高代碼的重用性、較少代碼的冗余
目前我總結(jié)的一共有6種繼承方式
- 原型鏈繼承
- 借用構(gòu)造函數(shù)繼承
- 組合式繼承(原型鏈+構(gòu)造函數(shù))
- 原型式繼承
- 寄生式繼承
- 寄生組合式繼承
function Person(name){
this.name = name;
this.sum=function(){
alert('this.name',this.name)
}
}
Person.prototype.age = 100
原型鏈繼承
實(shí)現(xiàn)方式: 利用原型鏈的特點(diǎn)繼承,讓實(shí)例的原型等于父類的實(shí)例
優(yōu)點(diǎn): 實(shí)例可以繼承父類的構(gòu)造個函數(shù),實(shí)例的構(gòu)造函數(shù),父類的原型
缺點(diǎn): 不能向父類傳遞參數(shù),由于實(shí)例的原型等于父類的實(shí)例,那么改變父類的屬性,實(shí)例的屬性也會跟著改變
function child(){
this.name="xiaoming"
}
child.prototype = new Person()
let child1 = new Child()
child1.name //xiaoming
child1.age //100
child1 instanceof Person //true
借用構(gòu)造函數(shù)繼承
實(shí)現(xiàn)方式: 使用call/apply將父類的構(gòu)造函數(shù)引入子類函數(shù)
優(yōu)點(diǎn): 可以禰補(bǔ)原型鏈繼承的缺點(diǎn),可以向父類傳遞參數(shù),只繼承父類構(gòu)造函數(shù)的屬性
缺點(diǎn): 不能復(fù)用,每次使用需要重新調(diào)用,每個實(shí)例都是父類構(gòu)造函數(shù)的副本,比較臃腫
function child(){
Person.call(this,'xiaoming')
}
let child1 = new child()
child1.name //xiaoming
child1.age //100
child1 instanceof Person //false
組合式繼承
實(shí)現(xiàn)方式: 復(fù)用+可傳遞參數(shù)
優(yōu)點(diǎn): 基于原型鏈的優(yōu)點(diǎn)和借用構(gòu)造函數(shù)的優(yōu)點(diǎn)
缺點(diǎn): 調(diào)用兩遍父類函數(shù)
function child(){
Person.call(this,'xiaoming')
}
child.prototype = new Person
let child1 = new child()
child1.name //xiaoming
child1.age //100
child1 instanceof Person //true
child instanceof Person //false原型式繼承
實(shí)現(xiàn)方式: 函數(shù)包裝對象,返回對象的引用,這個函數(shù)就變成可以隨時添加實(shí)例或者對象,Object.create()就是這個原理
優(yōu)點(diǎn): 復(fù)用一個對象用函數(shù)包裝
缺點(diǎn): 所有實(shí)例都繼承在原型上面 無法復(fù)用
function child(obj){
function F(){}
F.prototype = obj
return new F()
}
let child1 = new Person()
let child2 = child(child1)
child2.age //100寄生式繼承
實(shí)現(xiàn)方式: 在原型式繼承外面包了一個殼子
優(yōu)點(diǎn): 創(chuàng)建一個新對象
缺點(diǎn): 沒有用到實(shí)例 無法復(fù)用
function child(obj){
function F(){}
F.prototype = obj
return new F()
}
let child1 = new Person()
function subObject(){
let sub =child(child1)
sub.name='xiaoming'
return sub
}
let child2 = subObject(child1)
typeof subObject //function
typeof child2 //object
child2.age //100寄生組合式繼承
實(shí)現(xiàn)方式: 在函數(shù)內(nèi)返回對象的調(diào)用
優(yōu)點(diǎn): 函數(shù)的實(shí)例等于另外的一個實(shí)例,使用call/apply引入另一個構(gòu)造函數(shù),可傳遞參數(shù),修復(fù)了組合繼承的問題
缺點(diǎn): 無法復(fù)用
function child(obj){
function F(){}
F.prototype = obj
return new F()
}
let child1 = child(Person.prototype)
function Sub(){
Person.call(this)
}
Sub.prototype = child
child.constructor = Sub
let sub1 = new Sub()
sub1.age //100到此這篇關(guān)于JavaScript三大重點(diǎn)同步異步與作用域和閉包及原型和原型鏈詳解的文章就介紹到這了,更多相關(guān)JavaScript 同步異步內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于javascript 回調(diào)函數(shù)中變量作用域的討論
關(guān)于回調(diào)函數(shù)中變量作用域的討論精品推薦,大家可以參考下。2009-09-09
JavaScript隨機(jī)數(shù)生成各種技巧及實(shí)例代碼
這篇文章主要介紹了JavaScript隨機(jī)數(shù)生成各種技巧及實(shí)例代碼,包括生成0到1之間的隨機(jī)浮點(diǎn)數(shù)、指定范圍的隨機(jī)整數(shù)和浮點(diǎn)數(shù)、從數(shù)組中隨機(jī)選擇元素、生成隨機(jī)顏色以及生成指定數(shù)目和范圍的隨機(jī)數(shù),文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04
js插件YprogressBar實(shí)現(xiàn)漂亮的進(jìn)度條效果
ProgressBar.js 是一個借助動態(tài) SVG 路徑的漂亮的,響應(yīng)式的進(jìn)度條效果。使用 ProgressBar.js 可以很容易地創(chuàng)建任意形狀的進(jìn)度條。這個 JavaScript 庫提供線條,圓形和方形等幾個內(nèi)置的形狀,但你可使用 Illustrator 或任何其它的矢量圖形編輯器創(chuàng)建自己的進(jìn)度條效果。2015-04-04
解決前端接收 type:"application/octet-stream" 格
前端接收 type: “application/octet-stream“ 格式的數(shù)據(jù)并下載,還有后端既返回octet-stream還返回JSON數(shù)據(jù)時的處理方法,今天給大家分享前端接收 type:"application/octet-stream" 格式的數(shù)據(jù)并下載(解決后端返回不唯一問題)的解決方案,感興趣的朋友一起看看吧2023-12-12

