您現在的位置是:首頁 > 棋牌
前端技術:JavaScript進階篇
- 由 全棧技術部落格週刊 發表于 棋牌
- 2022-03-17
前端js是什麼意思
JavaScript高階
1。`this`關鍵字
this
關鍵字是一個非常重要的語法點。毫不誇張地說,不理解它的含義,大部分開發任務都無法完成。
JavaScript 語言之所以有 this 的設計,跟記憶體裡面的資料結構有關係。
var obj = { foo: 5 };
上面的程式碼將一個物件賦值給變數obj。JavaScript 引擎會先在記憶體裡面,生成一個物件{ foo: 5 },然後把這個物件的記憶體地址賦值給變數obj。也就是說,變數obj是一個地址(reference)。後面如果要讀取obj。foo,引擎先從obj拿到記憶體地址,然後再從該地址讀出原始的物件,返回它的foo屬性。
原始的物件以字典結構儲存,每一個屬性名都對應一個屬性描述物件。JavaScript 允許在函式體內部,引用當前環境的其他變數。
var f = function () {
console。log(x);
};
上面程式碼中,函式體裡面使用了變數x。該變數由執行環境提供。
現在問題就來了,由於函式可以在不同的執行環境執行,所以需要有一種機制,能夠在函式體內部獲得當前的執行環境(
context
)。所以,
this
就出現了,它的設計目的就是在函式體內部,指代函式當前的執行環境。
var f = function () {
console。log(this。x);
}
上面程式碼中,函式體裡面的this。x就是指當前執行環境的x。
this
使用的場合
1。
this
全域性環境使用:它指的就是頂層物件
window
2。
this
可以用在建構函式之中,表示例項物件。
var person = {
name: ‘張三’,
describe: function () {
return‘姓名:’+ this。name; // this 就是person物件
}
};
person。describe()
// “姓名:張三”
3。
this
在物件的方法:指向就是方法執行時所在的物件
2。call、apply、bind的區別
this
的動態切換,固然為
JavaScript
創造了巨大的靈活性,但也使得程式設計變得困難和模糊。有時,需要把
this
固定下來,避免出現意想不到的情況。
JavaScript
提供了
call
、
apply
、
bind
這三個方法,來切換/固定
this
的指向。
1。`Function。prototype。call()`函式例項的`call`方法
var obj = {};
var f = function () {
returnthis;
};
f() === window// true
f。call(obj) === obj // true
call
方法的引數,應該是一個物件。如果引數為空、
null
和
undefined
,則預設傳入全域性物件。
var n = 123;
var obj = { n: 456 };
functiona() {
console。log(this。n);
}
a。call() // 123
a。call(null) // 123
a。call(undefined) // 123
a。call(window) // 123
a。call(obj) // 456
call方法還可以接受多個引數。
func。call(thisValue, arg1, arg2, 。。。)
call的第一個引數就是this所要指向的那個物件,後面的引數則是函式呼叫時所需的引數。
functionadd(a, b) {
return a + b;
}
add。call(this, 1, 2) // 3
上面程式碼中,call方法指定函式add內部的this綁定當前環境(物件),並且引數為1和2,因此函式add執行後得到3。
2。Function。prototype。apply()
apply
方法的作用與
call
方法類似,也是改變
this
指向,然後再呼叫該函式。唯一的區別就是,它接收一個數組作為函式執行時的引數,使用格式如下。
func。apply(thisValue, [arg1, arg2, 。。。])
functionf(x, y){
console。log(x + y);
}
f。call(null, 1, 1) // 2
f。apply(null, [1, 1]) // 2
call()和apply()這兩個方法都是函式物件的方法,需要透過函式物件來呼叫。
當函式呼叫call()和apply()時,函式都會立即
執行
。
都可以用來改變函式的this物件的指向。
第一個引數都是this要指向的物件(函式執行時,this將指向這個物件),後續引數用來傳實參。
JS提供的絕大多數函式以及我們自己建立的所有函式,都可以使用call 和apply方法。它們的第一個引數是一個物件。因為你可以直接指定 this 繫結的物件,因此我們稱之為顯式繫結。
例1:
functionfoo() {
console。log(this。a);
}
var obj = {
a: 2
};
// 將 this 指向 obj
foo。apply(obj); //列印結果:2
第一個引數的傳遞
1、thisObj不傳或者為null、undefined時,函式中的this會指向window物件(非嚴格模式)。
2、傳遞一個別的函式名時,函式中的this將指向這個
函式的引用
。
3、傳遞的值為數字、布林值、字串時,this會指向這些基本型別的包裝物件Number、Boolean、String。
4、傳遞一個物件時,函式中的this則指向傳遞的這個物件。
call()和apply()的區別
call()和apply()方法都可以將實參在物件之後依次傳遞,但是apply()方法需要將實參封裝到一個
陣列
中統一傳遞(即使只有實參只有一個,也要放到陣列中)。
var persion1 = {
name: “小王”,
gender: “男”,
age: 24,
say: function (school, grade) {
alert(this。name + “ , ” + this。gender + “ ,今年” + this。age + “ ,在” + school + “上” + grade);
}
}
var person2 = {
name: “小紅”,
gender: “女”,
age: 18
}
call呼叫
persion1。say。call(persion2, “實驗小學”, “六年級”);
apply呼叫
persion1。say。apply(persion2, [“實驗小學”, “六年級”]);
call()和apply()的作用
改變this的指向
實現繼承。Father。call(this)
3。Function。prototype。bind()
bind
方法用於將函式體內的
this
繫結到某個物件,然後返回一個新函式。
都能改變this的指向
call()/apply()是
立即呼叫函式
bind()是將函式返回,因此後面還需要加
()
才能呼叫。
bind()傳參的方式與call()相同。
2。嚴格模式
嚴格模式是從 ES5 進入標準的,主要目的有以下幾個。
明確禁止一些不合理、不嚴謹的語法,減少 JavaScript 語言的一些怪異行為。
增加更多報錯的場合,消除程式碼執行的一些不安全之處,保證程式碼執行的安全。
提高編譯器效率,增加執行速度。
為未來新版本的 JavaScript 語法做好鋪墊。
總之,嚴格模式體現了 JavaScript 更合理、更安全、更嚴謹的發展方向。
進入嚴格模式的標誌,是一行字串
use strict
,嚴格模式可以用於整個指令碼,也可以只用於單個函式。
嚴格模式必須從程式碼一開始就生效
‘use strict’;
console。log(‘這是嚴格模式’);
console。log(‘這是正常模式’);
// 函式嚴格模式
functionstrict() {
‘use strict’;
return‘這是嚴格模式’;
}
functionstrict2() {
‘use strict’;
functionf() {
return‘這也是嚴格模式’;
}
return f();
}
functionnotStrict() {
return‘這是正常模式’;
}
3。非同步操作概述
單執行緒模型指的是,
JavaScript
只在一個執行緒上執行。也就是說,JavaScript 同時只能執行一個任務,其他任務都必須在後面排隊等待。
注意,
JavaScript
只在一個執行緒上執行,不代表 JavaScript 引擎只有一個執行緒。事實上,
JavaScript
引擎有多個執行緒,單個指令碼只能在一個執行緒上執行(稱為主執行緒),其他執行緒都是在後臺配合。
這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段 JavaScript 程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。JavaScript 語言本身並不慢,慢的是讀寫外部資料,比如等待 Ajax 請求返回結果。這個時候,如果對方伺服器遲遲沒有響應,或者網路不通暢,就會導致指令碼的長時間停滯。
如果排隊是因為計算量大,CPU 忙不過來,倒也算了,但是很多時候 CPU 是閒著的,因為 IO 操作(輸入輸出)很慢(比如 Ajax 操作從網路讀取資料),不得不等著結果出來,再往下執行。JavaScript 語言的設計者意識到,這時 CPU 完全可以不管 IO 操作,掛起處於等待中的任務,先執行排在後面的任務。等到 IO 操作返回了結果,再回過頭,把掛起的任務繼續執行下去。這種機制就是 JavaScript 內部採用的“事件迴圈”機制(Event Loop)。
單執行緒模型雖然對 JavaScript 構成了很大的限制,但也因此使它具備了其他語言不具備的優勢。如果用得好,JavaScript 程式是不會出現堵塞的,這就是為什麼 Node 可以用很少的資源,應付大流量訪問的原因。
為了利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準,允許 JavaScript 指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作 DOM。所以,這個新標準並沒有改變 JavaScript 單執行緒的本質。
同步任務(synchronous)和非同步任務(asynchronous)
同步任務是那些沒有被引擎掛起、在主執行緒上排隊執行的任務。只有前一個任務執行完畢,才能執行後一個任務。
非同步任務是那些被引擎放在一邊,不進入主執行緒、而進入任務佇列的任務。只有引擎認為某個非同步任務可以執行了(比如 Ajax 操作從伺服器得到了結果),該任務(採用回撥函式的形式)才會進入主執行緒執行。排在非同步任務後面的程式碼,不用等待非同步任務結束會馬上執行,也就是說,非同步任務不具有“堵塞”效應。
任務佇列和事件迴圈【很像Android的訊息傳遞機制】
JavaScript 執行時,除了一個正在執行的主執行緒,引擎還提供一個任務佇列(task queue),裡面是各種需要當前程式處理的非同步任務。(實際上,根據非同步任務的型別,存在多個任務佇列。為了方便理解,這裡假設只存在一個佇列。)
首先,主執行緒會去執行所有的同步任務。等到同步任務全部執行完,就會去看任務佇列裡面的非同步任務。如果滿足條件,那麼非同步任務就重新進入主執行緒開始執行,這時它就變成同步任務了。等到執行完,下一個非同步任務再進入主執行緒開始執行。一旦任務佇列清空,程式就結束執行。
非同步任務的寫法通常是回撥函式。一旦非同步任務重新進入主執行緒,就會執行對應的回撥函式。如果一個非同步任務沒有回撥函式,就不會進入任務佇列,也就是說,不會重新進入主執行緒,因為沒有用回撥函式指定下一步的操作。
JavaScript 引擎怎麼知道非同步任務有沒有結果,能不能進入主執行緒呢?答案就是引擎在不停地檢查,一遍又一遍,只要同步任務執行完了,引擎就會去檢查那些掛起來的非同步任務,是不是可以進入主執行緒了。這種迴圈檢查的機制,就叫做事件迴圈(Event Loop)。
非同步操作的模式:
採用回撥函式;
採用事件監聽;
釋出/訂閱;
JavaScript定時器
JavaScript
提供定時執行程式碼的功能,叫做定時器
(timer)
,主要由
setTimeout()
和
setInterval()
這兩個函式來完成。它們向任務佇列新增定時任務。
setTimeout
函式用來指定某個函式或某段程式碼,在多少毫秒之後執行。它返回一個整數,表示定時器的編號,以後可以用來取消這個定時器。
var timerId = setTimeout(func|code, delay);
setInterval函式的用法與setTimeout完全一致,區別僅僅在於setInterval指定某個任務每隔一段時間就執行一次,也就是無限次的定時執行。
// setTimeout
console。log(1);
setTimeout(timeOutPut, 100);
console。log(3);
// 1
//3
//定時器執行100毫秒輸出:2
functiontimeOutPut() {
console。log(“定時器執行100毫秒輸出:”+2);
}
var count = 0;
var consoleInterval = setInterval(function () {
count++;
console。log(“迴圈輸出%d,次數%d”, 2, count);
if (count === 10) {
clearInterval(consoleInterval);
}
}, 1000);
迴圈輸出2,次數%count 1
迴圈輸出2,次數%count 2
迴圈輸出2,次數%count 3
迴圈輸出2,次數%count 4
迴圈輸出2,次數%count 5
迴圈輸出2,次數%count 6
迴圈輸出2,次數%count 7
迴圈輸出2,次數%count 8
迴圈輸出2,次數%count 9
迴圈輸出2,次數%count 10
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
setTimeout
和
setInterval
函式,都返回一個整數值,表示計數器編號。將該整數傳入
clearTimeout
和
clearInterval
函式,就可以取消對應的定時器。
Promise 物件
Promise
物件是
JavaScript
的非同步操作解決方案,為非同步操作提供統一介面。它起到代理作用
(proxy)
,充當非同步操作與回撥函式之間的中介,使得非同步操作具備同步操作的介面。
Promise
可以讓非同步操作寫起來,就像在寫同步操作的流程,而不必一層層地巢狀回撥函式。
1。
Promise
是一個物件,也是一個建構函式。
Promise
設計思想:所有非同步任務都返回一個
Promise
例項。
Promise
例項有一個
then
方法,用來指定下一步的回撥函式。
functionf1(resolve, reject) {
// 非同步程式碼。。。
}
var p1 = newPromise(f1);
// f1的非同步操作執行完成,就會執行f2
p1。then(f2);
Promise
物件透過自身的狀態,來控制非同步操作。
Promise
例項具有三種狀態。
非同步操作未完成(pending)
非同步操作成功(fulfilled)
非同步操作失敗(rejected)
上面三種狀態裡面,
fulfilled
和
rejected
合在一起稱為
resolved
(已定型)。
var promise = newPromise(function (resolve, reject) {
// 。。。
if (/* 非同步操作成功 */){
resolve(value);
} else { /* 非同步操作失敗 */
reject(newError());
}
});
Promise
建構函式接受一個函式作為引數,該函式的兩個引數分別是
resolve
和
reject
。它們是兩個函式,由
JavaScript
引擎提供,不用自己實現。
resolve
函式的作用是,將
Promise
例項的狀態從“未完成”變為“成功”(即從
pending
變為
fulfilled
),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去。
reject
函式的作用是,將
Promise
例項的狀態從“未完成”變為“失敗”(即從
pending
變為
rejected
),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise 的優點
1。讓回撥函式變成了規範的鏈式寫法,程式流程可以看得很清楚。它有一整套介面,可以實現許多強大的功能,比如同時執行多個非同步操作,等到它們的狀態都改變以後,再執行一個回撥函式;再比如,為多個回撥函式中丟擲的錯誤,統一指定處理方法等等。
2。Promise 還有一個傳統寫法沒有的好處:它的狀態一旦改變,無論何時查詢,都能得到這個狀態。這意味著,無論何時為 Promise 例項添加回調函式,該函式都能正確執行。所以,你不用擔心是否錯過了某個事件或訊號。如果是傳統寫法,透過監聽事件來執行回撥函式,一旦錯過了事件,再添加回調函式是不會執行的。
4。作用域和閉包
1.執行上下文
執行上下文主要有兩種情況:
全域性程式碼:一段
script
標籤裡,有一個全域性的執行上下文。所做的事情是:變數定義、函式宣告【
在執行全域性程式碼前將window確定為全域性執行上下文。
】
函式程式碼:每個函數里有一個上下文。所做的事情是:變數定義、函式宣告、this、arguments【
在呼叫函式, 準備執行函式體之前, 建立對應的函式執行上下文物件(虛擬的, 存在於棧中)。
】
2.this
this
指的是,呼叫函式的那個物件。
this
永遠指向函式執行時所在的物件。
解析器在呼叫函式每次都會向函式內部傳遞進一個隱含的引數,這個隱含的引數就是this。
根據函式的呼叫方式的不同,this會指向不同的物件:【重要】
1。以函式的形式呼叫時,this永遠都是window。比如fun();相當於window。fun();
2。以方法的形式呼叫時,this是呼叫方法的那個物件
3。以建構函式的形式呼叫時,this是新建立的那個物件
4。使用call和apply呼叫時,this是指定的那個物件
需要特別提醒的是:this的指向在函式定義時無法確認,只有函式執行時才能確定。
3.作用域
作用域指一個變數的作用範圍。它是靜態的(相對於上下文物件), 在編寫程式碼時就確定了。 作用:隔離變數,不同作用域下同名變數不會有衝突。
作用域的分類:
全域性作用域
函式作用域
沒有塊作用域(ES6有了)
if (true) {
var name = ‘smyhvae’;
}
console。log(name);
上方程式碼中,並不會報錯,因為:雖然 name 是在塊裡面定義的,但是 name 是全域性變數。
全域性作用域:直接編寫在script標籤中的JS程式碼,都在全域性作用域。
在全域性作用域中:
在全域性作用域中有一個全域性物件window,它代表的是一個瀏覽器的視窗,它由瀏覽器建立我們可以直接使用。
建立的變數都會作為window物件的屬性儲存。
建立的函式都會作為window物件的方法儲存。
全域性作用域中的變數都是全域性變數,在頁面的任意的部分都可以訪問到。
變數的宣告提前:(變數提升)
使用
var
關鍵字宣告的變數( 比如
var a = 1
),會在所有的程式碼執行之前被宣告(但是不會賦值),但是如果宣告變數時不是用
var
關鍵字(比如直接寫a = 1),則變數不會被宣告提前。
舉例1:
console。log(a);
var a = 123;
列印結果:undefined
舉例2:
console。log(a);
a = 123; //此時a相當於window。a
程式會報錯:
函式的宣告提前:
使用
函式宣告
的形式建立的函式
function foo(){}
,
會被宣告提前
。
也就是說,它會在所有的程式碼執行之前就被建立,所以我們可以在函式宣告之前,呼叫函式。
使用
函式表示式
建立的函式
var foo = function(){}
,
不會被宣告提前
,所以不能在宣告前呼叫。
很好理解,因為此時foo被聲明瞭,且為undefined,並沒有給其賦值
function(){}
。
所以說,下面的例子,會報錯:
函式作用域
呼叫函式時建立函式作用域,函式執行完畢以後,函式作用域銷燬。
每呼叫一次函式就會建立一個新的函式作用域,他們之間是互相獨立的。
在函式作用域中可以訪問到全域性作用域的變數,在全域性作用域中無法訪問到函式作用域的變數。
在函式中要訪問全域性變數可以使用window物件。(比如說,全域性作用域和函式作用域都定義了變數a,如果想訪問全域性變數,可以使用
window。a
)
提醒1:
在函式作用域也有宣告提前的特性:
使用var關鍵字宣告的變數,是在函式作用域內有效,而且會在函式中所有的程式碼執行之前被宣告
函式宣告也會在函式中所有的程式碼執行之前執行
因此,在函式中,沒有var宣告的變數都會成為
全域性變數
,而且並不會提前宣告。
舉例1:
var a = 1;
functionfoo() {
console。log(a);
a = 2; // 此處的a相當於window。a
}
foo();
console。log(a); //列印結果是2
上方程式碼中,foo()的列印結果是
1
。如果去掉第一行程式碼,列印結果是
Uncaught ReferenceError: a is not defined
提醒2:
定義形參就相當於在函式作用域中聲明瞭變數。
functionfun6(e) {
console。log(e);
}
fun6(); //列印結果為 undefined
fun6(123);//列印結果為123
作用域與執行上下文的區別
區別1:
全域性作用域之外,每個函式都會建立自己的作用域,作用域在函式定義時就已經確定了。而不是在函式呼叫時
全域性執行上下文環境是在全域性作用域確定之後, js程式碼馬上執行之前建立
函式執行上下文是在呼叫函式時, 函式體程式碼執行之前建立
區別2:
作用域是靜態的, 只要函式定義好了就一直存在, 且不會再變化
執行上下文是動態的, 呼叫函式時建立, 函式呼叫結束時就會自動釋放
執行上下文(物件)是從屬於所在的作用域
全域性上下文環境==>全域性作用域
函式上下文環境==>對應的函式使用域
作用域鏈
當在函式作用域操作一個變數時,它會先在自身作用域中尋找,如果有就直接使用(
就近原則
)。如果沒有則向上一級作用域中尋找,直到找到全域性作用域;如果全域性作用域中依然沒有找到,則會報錯
ReferenceError
。
外部函式定義的變數可以被內部函式所使用,反之則不行。
<!DOCTYPE html>
//只要是函式就可以創造作用域
//函式中又可以再建立函式
//函式內部的作用域可以訪問函式外部的作用域
//如果有多個函式巢狀,那麼就會構成一個鏈式訪問結構,這就是作用域鏈
//f1——->全域性
functionf1(){
//f2——->f1——->全域性
functionf2(){
//f3——>f2——->f1——->全域性
functionf3(){
}
//f4——->f2——->f1——>全域性
functionf4(){
}
}
//f5——->f1——>全域性
functionf5(){
}
}