JavaScript入門②-函式(1)基礎{淺出}

安木夕發表於2022-12-01

image.png

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?)是一種簡潔的函式表示式,顧名思義就是用箭頭=>建立函式,它沒有自己的thisargumentssuper。箭頭函式總是匿名的,適用於那些需要匿名函式的地方。

語法規則:(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); 列印一條錯誤資訊,類似的還有infowarn
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的變數、引數資訊。

image.png

因此,閉包就是為了解決了函式的詞法作用域問題,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的坑!好像懂了,又好像沒懂!詳見後續


©️版權申明:版權所有@安木夕,本文內容僅供學習,歡迎指正、交流,轉載請註明出處!原文編輯地址-語雀

相關文章