01、JS函式基礎
1.1、函式定義
函式(方法)就是一段定義好的邏輯程式碼,函式本身也是一個object
引用物件。三種函式構造方式:
?① 函式申明:function 函式名(引數){程式碼}
,申明函式有函式名提升的效果,先呼叫,後申明(和var申明提升類似,比var提升更靠前)。
?② 函式表示式:var func = function(引數){程式碼}
,定義變數指向函式,函式不需要命名。不過也可以像申明函式一樣指定函式名,在其內部呼叫自己。
?③ Function建構函式:new Function(引數,程式碼)
,支援多個構造引數,前面為函式引數,最後一個為函式體,使用不多。
function func1(a) {
console.log(a);
}
var func2 = function(b){
console.log(b);
}
var func3 = new Function('c','console.log(c)');
//呼叫函式
func1(1);
func2(2);
func3(3);
❗注意:JS中沒有方法過載,不允許相同的函式名,重名會被覆蓋。
?return 返回值:
- 透過
return
返回值,並結束方法。 - 無
return
,則預設返回undefined
。
1.2、引數argument
- 引數可以不傳,則為
undefined
,也可多傳,沒鳥用(也不一定,arguments
引數陣列可以用)。 - 引數不可同名,如果同名,則後面為準。
- 形參與實參:函式定義的引數
a
為形參,呼叫時傳入的資料3
為實參。 - 引數設定預設值幾種方式:形參賦預設值(ES6)、引數驗證賦值。
function func1(a="預設值") { //一般推薦的方式
a=a?a:"預設值"; //引數為null、undefined、0、false、空字元值都會用預設值
a=a||"預設值"; //同上
console.log(a);
}
- 引數的值傳遞和引用傳遞:取決於引數的型別,值型別傳遞副本,引用型別傳遞物件指標地址,操作的是同一個物件。這裡需要理解JS裡面的值型別、引用型別的基本原理。
function f1(n) {
n += 1;
}
let n = 100;
f1(n); //值傳遞:傳入的是n的值副本,不影響原本n值
console.log(n); //100 n沒有變
function f2(obj) {
obj.n += 1;
}
let nobj = { n: 100 };
f2(nobj); //引用傳遞,傳入的是nobj地址指標,操作同一物件,物件是共享的
console.log(nobj); //{n: 101}
- arguments:函式傳入的實參都儲存在
arguments
陣列物件中,對於任意數量引數的方法就很有用。
function sum() { //求和
let n = 0;
for (let i = 0; i < arguments.length; i++) {
n += arguments[i];
}
return n;
}
sum(1,2,3,108,594); //求和,支援任意個引數
- 剩餘引數(...theArgs):把不確定的引數放到一個陣列裡,前面是確定引數,最後一個形參以
...
開頭表示其他剩餘引數陣列,既然是陣列,當然就可以支援任意數量的引數了。
// 連線字串,支援任意數量字元引數
function connect(separator, ...sargs) {
let str = "";
for (let i = 0; i < sargs.length; i++) {
str += sargs[i]?.toString() + separator;
}
return str.slice(0, -separator?.toString().length); //去掉結尾的分隔符
}
connect('--', 1, 2, 3, 'a', 'b', 'c'); //1--2--3--a--b--c
1.3、函式(變數)作用域
- 區域性變數:函式內的變數稱為“區域性變數”,函式里才有作用域的問題——不能被全域性、其他函式訪問。
- 全域性變數:全域性變數可以被自由訪問,包括函式內
- 父函式變數:函式中定義的函式也可以訪問在其父函式中定義的所有變數,和父函式有權訪問的任何其他變數。簡單來說就是函式變數作用域單向向下傳遞,子級可以訪問父級的變數。
var num1 = 0;
function getScore() {
let num1 = 2, //和全域性變數同名
num2 = 3;
function add() {
num3 = 5; //沒用任何申明的區域性變數,使用後自動變為隱式全域性變數
return num1 + num2; //可以訪問全域性、父級的變數
}
return add();
}
getScore(); //5
console.log(num1, num3); //0 5
⁉️注意:
- 全域性、區域性(方法內)變數同名,兩者沒有關係,函式內肯定就近用自己的了。
- 沒用任何申明的區域性變數,使用後自動變為隱式全域性變數,全域性
window
莫名其妙就有了很多私生子,so,不要這樣幹!
1.4、()=>{ }箭頭函式
箭頭函式(IE?)是一種簡潔的函式表示式,顧名思義就是用箭頭=>
建立函式,它沒有自己的this
、arguments
、super
。箭頭函式總是匿名的,適用於那些需要匿名函式的地方。
語法規則:(param1, param2, …, paramN) => { ... }
let add1 = (a, b) => { return a + b; };
let add = (a, b) => a + b; //同上,只有一行程式碼可以省略花括號{} 和 return
let logDate = () => { console.log(new Date()) }; //可以沒有引數
let logError = e => { console.log('發生錯誤:', e) }; //一個引數可以省略括號
=>
箭頭函式和普通函式有什麼區別呢?這是考點:
區別 | 描述 |
---|---|
申明方式不同 | 普通函式需要function 關鍵字,箭頭函式當然就是箭頭=> 了 |
沒有自己的arguments |
箭頭函式在全域性環境中,沒有arguments ;當箭頭函式處於普通函式的中,arguments 是上層普通函式的arguments |
沒有自己的this |
沒有自己的this,其this 指向其函式定義的外層作用域環境的this ,且不能被call、apply、bind函式改變。 |
沒有自己的prototype |
箭頭函式沒有自己的原型,加上沒有自己的this ,所以也就不能作為建構函式使用 |
// 箭頭函式
let f1 = (a, b) => {
console.dir(f1.prototype); //undefined
console.log(arguments); //arguments is not defined
}
// 普通函式,巢狀了一個箭頭函式
let f2 = function (a) {
console.dir(f2.prototype); //Object
// 巢狀的箭頭函式
let f1 = (a, b) => {
console.log(arguments);
} //是父級f2的arguments
f1(a, 2);
}
//this
var name = '大哥';
let user = {
name: 'sam',
sayHi1: function () {
console.log('Hi', this.name)
},
sayHi2: () => { console.log('Hi', this.name) },
}
user.sayHi1(); //Hi sam :this指向呼叫者
user.sayHi2(); //Hi 大哥 :this指向定義的環境,全域性物件
// 指定新的this
let nobj={name:'張三'};
user.sayHi1.call(nobj); //Hi 張三 :this指向繫結物件
user.sayHi2.call(nobj); //Hi 大哥 :this指向全域性物件,始終沒變
1.5、全域性函式
函式 | 描述 |
---|---|
eval() | 執行JS程式碼(不推薦):eval("console.log('eval')"); - 比較危險,它使用與呼叫者相同的許可權執行程式碼,字串程式碼容易被被惡意修改。 - 效率低,它必須先呼叫JS解釋,也沒有其他最佳化,還會去查詢其他JS程式碼(變數)。 - 推薦用 Function() 建構函式代替eval() |
isNaN() | 判斷一個值是否是非數值 |
parseFloat() | 轉換字元為浮點數 |
parseInt() | 轉換字元為整數 |
decodeURI() | URL解碼 |
encodeURI() | URL編碼 |
alert(str) | 彈窗訊息提示框 |
confirm(str) | 彈窗訊息詢問確認框,返回boolean |
console | 控制檯輸出 |
console.log(str) | 控制檯輸出一條訊息 |
console.error(str); | 列印一條錯誤資訊,類似的還有info 、warn |
console.dir(object) | 列印物件 |
console.trace() | 輸出堆疊 |
console.time(label) | 計時器,用於計算耗時(毫秒):time(開始計時) > timeLog(計時) > timeEnd(結束) |
console.clear() | 清空控制檯,並輸出 Console was cleared。 |
let arr = eval('[1,2,3]'); //轉換字串為陣列
let jobj = eval("({name:'sam',age:22})"); //轉換字串為JSON物件
let jobj2 = new Function("return {name:'sam',age:22}")(); //轉換字串為JSON物件
//計時time,需一個統一標誌
console.time("load");
console.timeLog("load"); //load: 5860ms
console.timeLog("load"); //load: 18815ms
console.timeEnd("load"); //load:25798 毫秒 - 倒數計時結束
02、函式呼叫/call/apply/bind
常用的函式呼叫方式:
- 直接函式名呼叫:
函式名(引數...);
- 物件呼叫:物件裡的函式,
物件.函式名(引數...);
- 遞迴呼叫,巢狀呼叫自身,須注意退出機制,避免死迴圈,程式碼的世界沒有天荒地老。
不常用函式呼叫方式:call/apply/bind 呼叫函式都可以指定this
值。
屬性/方法 | 描述 | 語法 |
---|---|---|
call() | 呼叫函式,指定this 、引數 |
function.call(thisArg, arg1, arg2, ...) |
apply() | 呼叫函式,指定this 、引數陣列 |
function.apply(thisArg, argsArray) |
bind() | 繫結(複製)一個函式,指定this 、引數 |
function.bind(thisArg, arg1, arg2, ...) |
?call()
透過 Function.prototype.call() 呼叫一個函式,呼叫語法:
function.call(thisArg, arg1, arg2, ...)
- 第一個引數
thisArg
指定執行時this
,當第一個引數為null、undefined的時候,預設指向window。這一點可用來實現函式的“繼承”(在建構函式中呼叫父建構函式) - 後面為函式原本的引數。
function sum(n1, n2) {
return n1 + n2;
}
sum(1,2); //正常調回
sum.call(null, 1, 2); //call呼叫
console.log(Math.max(1, 2)); //正常調回
console.log(Math.max.call(null, 1, 2)); //call呼叫
?apply()
透過 Function.prototype.apply() 呼叫函式,呼叫語法:
function.apply(thisArg, argsArray)
和call()
的唯一區別就是第二個引數是一個引數陣列,陣列引數會分別傳入原函式。
function sum(n1, n2) {
return n1 + n2;
}
sum.apply(null, [1, 2]); //apply呼叫
console.log(Math.max.apply(null, [5, 4, 2, 1, 22, 9])); //apply呼叫,陣列傳入多個引數
//繫結this
var uname = "sam";
let f = function () {
console.log(this.uname);
}
f(); //sam
f.call({ uname: "call" }); //call
f.apply({ uname: "apply" }); //apply
?bind()
透過 Function.prototype.bind() 建立一個副本函式:該副本函式繫結了this
和引數,且一經繫結,永恆不變(不可更改,不可二次繫結)。
function.bind(thisArg[, arg1[, arg2[, ...]]])
function log(type, title, message) {
console.log(`type:${type}, title:${title}, message:${message}`)
}
let errorLog = log.bind(null, '錯誤'); //返回繫結的函式,繫結第一個引數
errorLog('登入發生異常', '超過登入次數');
03、函式閉包
3.1、什麼是閉包?
閉包是函式和申明該函式的詞法環境的組合,簡單來說能夠訪問其他函式(通常是父函式)內部變數的函式,加上他引用的外部變數,組成了閉包。通常就是巢狀函式,巢狀函式可以”繼承“父函式的引數和變數,或者說內部函式包含外部函式的作用域。
- 內部函式+外部引用形成了一個閉包:它可以訪問外部函式的引數和變數。閉包儲存了自己和其作用域的變數,這樣在函式呼叫棧上才能使用外部函式的變數。
function FA(x) {
function FB(y) {
function FC(z) {
console.log(x + y + z);
}
FC(3);
console.dir(FC)
}
FB(2);
console.dir(FB)
}
FA(1); //6
console.dir(FA)
作用域鏈(C>B>A):B和A形成閉包,B可以訪問A,儲存了A的變數;C和B形成閉包,可以訪問B(也包括B有的A作用域),如下圖FC函式形成的閉包中儲存了FB、FA的變數、引數資訊。
因此,閉包就是為了解決了函式的詞法作用域問題,FC函式就可以單獨使用了。V8引擎是把閉包封裝成了一個"Closure"
物件,儲存函式上下文中的[[Scope]]
集合裡。同一個函式多次呼叫都會產生單獨的閉包,如果閉包使用不當或太多,容易引發記憶體洩漏風險。
JS設計閉包這個東西,一言難盡!詳見後續《函式執行上下文》
3.2、閉包應用:柯里化(Currying)
柯里化是一種函式的高階玩法,簡單來說,就是把多引數的函式f(a,b)
,轉換成了另一種函式形式 f(a)(b)
。
//一個普通的日誌函式
function print(title, message) {
console.log(title, message);
}
//柯里化轉化函式
function curry(func) {
return function (title) {
return function (message) {
return func(title, message);
}
}
}
//柯里化轉化
let cprint = curry(print);
//呼叫
cprint('使用者模組')('使用者1登入了');
//複用:複用包含了title引數值的函式。
let userPrint = cprint('使用者模組');
userPrint('使用者1退出登入');
userPrint('使用者3打賞了遊艇');
原理其實不難理解,就是利用閉包的(詞法作用域)機制,返回多層閉包函式,直到最後一個引數來了才執行。這麼做到底有什麼好處?——答案就是複用,複用引數值。如上面示例中的userPrint
,是一個包含了title
引數值的(閉包)函式,他還有一個正式的名字,叫偏函式。
一個更通用的柯里化實現如下,採用遞迴的方式,不僅可以生成任意偏函式,還支援正常呼叫。
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
//使用
let log = curry(print);
log('登入模組','系統崩了'); //正常呼叫
let userLog = log('直播模組'); //偏函式-複用
userLog('表演才藝');
userLog('上鍊接');
04、this關鍵字
this 是JS的關鍵字,指向當前執行的環境物件,允許函式內引用當前環境的其他變數,不同場景指向不同。在嚴格模式("use strict";
)下又會不同。大多數情況下,函式的呼叫方式決定了 this
的值(執行時繫結),每次呼叫函式的this
也可能不同。
this.x1 = 100;
console.log(this.x1); //這裡的this指向全域性物件window
function User() {
this.sname = "sam";
this.age = 20;
console.log(this.sname, this.age);
}
new User(); //sam 20,User建構函式里的this指向new建立的新User例項
this 指向的是一個物件引用,不是指向函式自身,也不是指向函式的詞法作用域。大多數情況下預設都指向全域性
window
- this=全域性window:在全域性執行環境中(在任何函式體外部)this 都指向全域性物件
window
。 - this=new物件:建構函式中的
this
指向其新物件;物件的屬性方法中的this
指向該物件。 - this=呼叫者:區域性(函式內的)this,誰呼叫函式,this指的就是誰。箭頭函式除外,箭頭函式本身沒有this,也不會接收call、apply的傳遞,指向其函式定義環境的
this
,而非執行時。 - this=事件元素:在事件中,this表示接收事件的元素。
- this=繫結物件:call(thisArg)、apply(thisArg)、bind(thisArg)繫結引數
thisArg
作為其上下文的this,若引數不是物件也會被強制轉換為物件,強扭的瓜解渴! - this=undefined:嚴格模式下,如果this沒有被執行環境(execution context)定義,那
this
就是undefined
。
function Foo() {
console.log(this);//呼叫Foo(),this指向window;如果new Foo()則指向新物件
var fa = () => { console.log("fa:" + this) };
var fb = function () {
console.log("fb:" + this);
}
fa(); //箭頭函式,呼叫Foo(),this指向呼叫者window;如果new Foo()則指向新物件
fb(); //匿名函式,呼叫Foo()、建構函式呼叫,this指向呼叫者window,
this.fc1 = fa; //屬性方法:this指新物件
}
Foo(); //呼叫Foo()函式
new Foo(); //建構函式呼叫創新例項
又是一個JS的坑!好像懂了,又好像沒懂!詳見後續
©️版權申明:版權所有@安木夕,本文內容僅供學習,歡迎指正、交流,轉載請註明出處!原文編輯地址-語雀