JavaScript中函數(shù)的四種調(diào)用方式總結(jié)
在《JavaScript忍者秘籍》書中提到,我們有四種不同的方式進(jìn)行函數(shù)調(diào)用:
- 作為一個(gè)函數(shù)進(jìn)行調(diào)用,這是最簡單的形式。
- 作為一個(gè)方法進(jìn)行調(diào)用,在對象上進(jìn)行調(diào)用,支持面向?qū)ο缶幊獭?/li>
- 作為構(gòu)造器進(jìn)行調(diào)用,創(chuàng)建一個(gè)新對象。
- 通過apply() 或call() 方法進(jìn)行調(diào)用。
作為一個(gè)曾經(jīng)的小鎮(zhèn)做題家,一些關(guān)鍵詞自然而然就冒出來了,this、閉包、原型......這些概念都不陌生,但總感覺有些散亂。我發(fā)現(xiàn),順著書中這幾種函數(shù)調(diào)用方式,層層遞進(jìn),倒是可以幫助更好的理解和串聯(lián)這些知識點(diǎn)。
1. 作為函數(shù)調(diào)用
這里指的就是最簡單直接的調(diào)用方式:
function sum(a, b) { return a + b; } sum(1, 2)
函數(shù)封裝了一段邏輯,給一個(gè)輸入,得到一個(gè)輸出。函數(shù)最大的好處是它是可以復(fù)用的,而不是在每個(gè)需要的地方都寫一段重復(fù)的代碼。用好函數(shù)往往可以讓代碼變得更清晰。
使用函數(shù)時(shí)必然要關(guān)注它的作用域。簡單來說,函數(shù)內(nèi)是局部變量,只能在這個(gè)函數(shù)中訪問,而全局變量可以在程序的任何代碼中訪問。但在JS中比這復(fù)雜,因?yàn)楹瘮?shù)是可以嵌套的,也可以作為參數(shù)傳遞。比如下面的例子:
function outer() { let value = 0; function inner(n) { value += n; return value; } return inner; } const fn = outer(); fn(1); // 1 fn(2); // 3
此時(shí)inner()
作為內(nèi)部函數(shù)是可以訪問到外部函數(shù)outer()
內(nèi)定義的局部變量的,即使它們是兩個(gè)不同的函數(shù)。另外,正常來說,一個(gè)函數(shù)執(zhí)行完成后,所有函數(shù)內(nèi)定義的局部變量都會被回收,但這里不同:outer()
執(zhí)行完成了,返回了一個(gè)函數(shù),而這個(gè)函數(shù)內(nèi)仍然能訪問value
這個(gè)局部變量(value
并沒有被回收)——即“閉包”。因?yàn)镴S支持更靈活的函數(shù)使用,所以也就引出了這些概念,這在Go語言中也是一摸一樣的。
2. 作為方法進(jìn)行調(diào)用
即函數(shù)作為對象的一個(gè)屬性,如下所示:
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello~"); }, }; dog.sayHi(); // Hello~
JS中可以用對象表示一些屬性的集合,比如一只狗有名字和年齡。當(dāng)然這個(gè)對象也可以有方法,比如這只狗會打招呼。如果想在打招呼的時(shí)候報(bào)出自己的名字怎么做呢?當(dāng)然我們可以直接用對象名訪問,即dog.name
,但還有一種更友好的方式——this
(在Java中,關(guān)鍵字“this”表示當(dāng)前對象的引用,而在JS中,“this”是支持面向?qū)ο缶幋a的主要手段之一)。
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; dog.sayHi(); // Hello,my name is wangcai
這樣就清晰多了,不用管變量名的dog是哪個(gè)dog,this就是本狗了。那普通函數(shù)不在對象中,它調(diào)用時(shí)的this是什么呢?
function getThis() { return this; } getThis() === window // true
在瀏覽器中,答案就是全局環(huán)境window。回到前面dog的例子,下面代碼中this又是什么呢?是本狗嗎?答案我們在后面揭曉。
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; const sayHi = dog.sayHi; sayHi();
3. 作為構(gòu)造器進(jìn)行調(diào)用
如果我有很多條狗,那么用對象來一個(gè)個(gè)聲明就略顯笨拙了。你可能會寫一個(gè)createDog
的函數(shù),來按模式批量生產(chǎn)dog。
function createDog(name, age) { return { name, age, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; } const dog = createDog("wangcai", 1);
但同樣,有一種更“面向?qū)ο?rdquo;,更友好的方式——構(gòu)造函數(shù)(constructor,ES6的“class”就是構(gòu)造函數(shù)的語法糖) 。
function Dog(name, age) { this.name = name; this.age = age; this.sayHi = function () { console.log("Hello,my name is ", this.name); }; } const dog = new Dog("wangcai", 1);
構(gòu)造函數(shù)需要搭配“new”關(guān)鍵字使用,它將自動返回一個(gè)新的對象。另外,構(gòu)造函數(shù)一般用大寫開頭,同時(shí)它應(yīng)該是一個(gè)名詞,而不是動詞。
構(gòu)造函數(shù)只是一個(gè)約定,語言本身并沒有限制你如何使用它。你可以直接執(zhí)行Dog("wangcai",i)
,但這樣沒有什么意義,還會產(chǎn)生一些意外的全局變量。
我們可以結(jié)合前一節(jié)的“對象方法”來理解構(gòu)造器的調(diào)用過程:
- 創(chuàng)建一個(gè)空對象
- 在該對象上執(zhí)行這個(gè)函數(shù)(函數(shù)調(diào)用時(shí)的this指向這個(gè)對象)
- 最后將這個(gè)對象返回,如果沒有顯式的返回值(還差了一個(gè)步驟,暫時(shí)沒涉及,后面再說)
JS中幾乎所有對象都有自己的構(gòu)造函數(shù),對于使用字面量語法聲明的對象,它的構(gòu)造函數(shù)就是Object。
const dog = { name: "wangcai" };
JS中有三種方式創(chuàng)建一個(gè)對象。
- 字面量語法,如
const obj = { value: 100 }
Object.create()
- 使用new調(diào)用構(gòu)造函數(shù)初始化一個(gè)對象
只有通過Object.create(null)
創(chuàng)建的對象是沒有構(gòu)造函數(shù)的,也沒有“原型”。
對象與構(gòu)造函數(shù)之間通過原型進(jìn)行關(guān)聯(lián)。還是以我們的dog為例:
const dog = new Dog("wangcai", 1); Object.getPrototypeOf(dog) === Dog.prototype // true
“當(dāng)構(gòu)造函數(shù)搭配new
使用時(shí),該函數(shù)的prototype數(shù)據(jù)屬性將用作新對象的原型。默認(rèn)情況下,函數(shù)的prototype是一個(gè)普通的對象。這個(gè)對象具有一個(gè)屬性:constructor,它是對這個(gè)函數(shù)本身的一個(gè)引用。 constructor 屬性是可編輯、可配置但不可枚舉的”。所以,我們通過Object.getPrototypeOf(dog).constructor
可以直接獲取到對象的構(gòu)造函數(shù)。下面就是瀏覽器控制臺打印出的dog對象:
那對象的原型又是什么呢?它是JS中一種獨(dú)特的機(jī)制,它的特點(diǎn)如下:
- 每個(gè)對象都有一個(gè)私有屬性指向另一個(gè)名為原型(prototype)的對象。當(dāng)訪問一個(gè)對象屬性時(shí),如果屬性不存在,就會繼續(xù)查找這個(gè)對象的原型屬性。
- 原型對象也有一個(gè)自己的原型,層層向上直到一個(gè)對象的原型為null。
Object.prototype
的原型始終為null且不可更改。
所以,一個(gè)對象不僅有實(shí)例屬性,還有原型屬性,它們都可以被訪問到,只是原型屬性是不可枚舉的。
const o = { value: 100}; Object.keys(o); // ['value'] o.valueOf(); // valueOf是構(gòu)造函數(shù)Object的prototype上定義的方法,可以正常訪問
再回顧new
進(jìn)行初始化的過程,是缺了什么步驟呢?就是將對象的原型指向構(gòu)造函數(shù)的prototype。我們可以按這個(gè)規(guī)則模擬一個(gè)new函數(shù)。
function Dog(name, age) { this.name = name; this.age = age; } Dog.prototype.sayHi = function () { console.log("Hello,my name is ", this.name); }; function myNew(fn, ...args) { const obj = Object.create(fn.prototype); // 創(chuàng)建一個(gè)對象,將對象的原型指向fn.prototype const res = fn.apply(obj, args); return res ?? obj; } const dog = myNew(Dog, "wangcai", 1); dog.sayHi(); // Hello,my name is wangcai Object.getPrototypeOf(dog) === Dog.prototype; // true
大家細(xì)心會發(fā)現(xiàn),這個(gè)例子中將sayHi
函數(shù)放在了Dog的prototype對象上,而不像之前在函數(shù)內(nèi)通過this.sayHi
聲明。兩種方式new出來的對象都是能調(diào)用sayHi()
的,唯一的區(qū)別是:一個(gè)是通過對象的實(shí)例屬性訪問,而另一個(gè)是通過原型屬性訪問。后者看起來是這樣的:
原型屬性具有一些優(yōu)點(diǎn),比如在這個(gè)例子中,每個(gè)new出來的對象訪問的都是同一個(gè)sayHi
函數(shù)(定義在Dog的prototype對象上),而不是重新拷貝,這樣節(jié)省內(nèi)存。同時(shí),sayHi
函數(shù)只需修改一次,就能應(yīng)用到所有實(shí)例化的對象中。
Dog.prototype.sayHi = function () { console.log("Good morning~"); }; dog.sayHi(); // Good morning~
原型鏈的特性還能支持我們實(shí)現(xiàn)對象的“繼承”。首先我們用class語法試下繼承的效果。
// 基類 class Animal { constructor(name) { this.name = name; } eat() { console.log(`${this.name} is eating`); } } //派生類 class Dog extends Animal { constructor(name, age) { super(name); this.age = age; } sayHi() { console.log(`Hello, my name is ${this.name}, I am ${this.age} years old`); } } const dog = new Dog("wangcai", 1); dog.sayHi(); // Hello, my name is wangcai, I am 1 years old dog.eat(); // wangcai is eating
分析一下,age
與name
都在dog對象的實(shí)例屬性上,而sayHi()
函數(shù)在dog對象的原型上,即Dog.prototype
。再往上一層,Dog.prototype
這個(gè)對象的原型是Animal.prototype
,Animal.prototype
上具有eat()
方法。那再往上一層呢?Animal.prototype這個(gè)對象的原型是Object,再往上就到原型鏈頂端null了。
接下來相信大家也有概念怎么手動實(shí)現(xiàn)繼承了。下面是一個(gè)示例:
function Animal(name) { this.name = name; } Animal.prototype.eat = function () { console.log(`${this.name} is eating`); }; function Dog(name, age) { this.age = age; Animal.call(this, name); // 將Animal的實(shí)例屬性放到Dog對象上 Object.setPrototypeOf(Dog.prototype, Animal.prototype); // 設(shè)置原型鏈 } Dog.prototype.sayHi = function () { console.log(`Hello, my name is ${this.name}, I am ${this.age} years old`); }; const dog = new Dog("wangcai", 1); dog.sayHi(); dog.eat();
4. 通過apply()或call()方法進(jìn)行調(diào)用
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; const sayHi = dog.sayHi; sayHi();
再回顧前面的例子,揭曉答案,結(jié)果是Hello,my name is undifined
,顯然不是本狗了。
為什么會這樣呢?因?yàn)镴S中函數(shù)的“this”是由調(diào)用點(diǎn)決定的(在運(yùn)行時(shí)確定) ,而不是在函數(shù)聲明處決定。這就讓人很困擾了,本狗的名字都對不上了,這樣真的好嗎?其實(shí)這樣是為了提供更大的靈活性——支持動態(tài)上下文。
function sayHi() { console.log("Hello,my name is ", this.name); } const dog = { name: "wangcai", sayHi, }; const cat = { name: "miaomiao", sayHi, }; dog.sayHi(); // Hello,my name is wangcai cat.sayHi(); // Hello,my name is miaomiao
通過允許this
由調(diào)用點(diǎn)動態(tài)確定,可以讓同一個(gè)函數(shù)在不同的對象上使用。另外,JS也提供了顯式指定函數(shù)執(zhí)行時(shí)的this為某個(gè)對象的方法,即apply()
或call()
。
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; const sayHi = dog.sayHi; sayHi.apply(dog); // Hello,my name is wangcai sayHi.call(dog); // Hello,my name is wangcai
很開心,本狗又回來了。apply()
與call()
只有函數(shù)傳參上的差異,在使用時(shí)看哪個(gè)方便用哪個(gè)就行。
總結(jié)
通過體驗(yàn)函數(shù)這四種不同的調(diào)用方式,我們逐漸接觸了JS中一些底層的知識點(diǎn):作為函數(shù)調(diào)用時(shí)的閉包,作為方法調(diào)用時(shí)的this指向,作為構(gòu)造器調(diào)用時(shí)的原型。JS中的函數(shù)非常靈活,也很強(qiáng)大,并且與對象有著密切的聯(lián)系。
以上就是JavaScript中函數(shù)的四種調(diào)用方式總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于JavaScript函數(shù)調(diào)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用mock.js隨機(jī)數(shù)據(jù)和使用express輸出json接口的實(shí)現(xiàn)方法
這篇文章主要介紹了使用mock.js隨機(jī)數(shù)據(jù)和使用express輸出json接口的實(shí)現(xiàn)方法,需要的朋友可以參考下2018-01-01ES6學(xué)習(xí)筆記之Set和Map數(shù)據(jù)結(jié)構(gòu)詳解
這篇文章主要介紹了ES6學(xué)習(xí)筆記之Set和Map數(shù)據(jù)結(jié)構(gòu),結(jié)合實(shí)例形式詳細(xì)分析了ECMAScript中基本數(shù)據(jù)結(jié)構(gòu)Set和Map的常用屬性與方法的功能、用法及相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-04-04uni-app調(diào)取接口的3種方式以及封裝uni.request()詳解
我們在實(shí)際工作中要將數(shù)據(jù)傳輸?shù)椒?wù)器端,從服務(wù)器端獲取信息,都是通過接口的形式,下面這篇文章主要給大家介紹了關(guān)于uni-app調(diào)取接口的3種方式以及封裝uni.request()的相關(guān)資料,需要的朋友可以參考下2022-08-08