一千個讀者,有一千個哈姆雷特。html
我將會從函數的執行機制、魯棒性、函數式編程、設計模式等方面,全面闡述如何編寫高質量的函數。前端
如何編寫高質量的函數,這是一個很難回答的問題,不一樣人心中對高質量有本身的見解,這裏我將全面的闡述我我的對如何編寫高質量函數的一些見解。見解可能不夠全面,也可能會有一些錯誤的看法,歡迎一塊兒討論,就像過日子的人,小吵小鬧總會不經意的出現,一顆包容的心莫過因而最好的 best practice
。git
寫博客趨向意識流(胡謅流),按着我內心想的去寫,不會去詳細的說明某一個知識點,若是須要詳細討論,能夠在文末加我微信細聊。程序員
下面開始吧,我打算用三篇文章來完成 如何編寫高質量的函數 這個系列。github
三篇文章我將從如下幾個方面去闡述,如何編寫高質量的函數。面試
V8
友好的函數是一種什麼 style
下面開始胡謅啦:小聲 BB
,能夠先點個贊鼓勵一下麼(給一波精神上的鼓勵)。編程
PS: 寫過文章的小夥伴都知道,寫一篇好文很不容易,很消耗精力,而寫完博客後,最大的精神知足莫過於小夥伴的一個確定的贊。 哎,其實這個系列已經寫好了,原本想一篇文章發完的,可是看了下,
2
萬字,太多了,仍是分三篇吧。設計模式
本篇只說第一節 函數
,擒賊先擒王,下面咱們來盤一盤函數的七七八八,往 XXX
上盤😂。數組
函數二字,表明着一切皆有可能。微信
咱們想一下:咱們用的函數究竟離咱們有多遠。就像打麻將同樣,你以爲你能像雀神那樣,想摸啥就來啥麼(誇張修辭手法)。
每天和函數打交道,有沒有想過函數出現的目的是什麼?咱們再深刻想一下,函數的執行機制是什麼?下面咱們就來簡單的分析一下。
函數是迄今爲止發明出來的用於節約空間和提升性能的最重要的手段。
PS: 注意,沒有之一。
有句話說的好,知己知彼,百戰不殆。想要勝利,必定要很是的瞭解敵人。JS
確定不是敵人啦,可是要想掌握 JS
的函數,要更輕鬆的編寫高質量的函數,那就要去掌握在 JS
中,函數的執行機制。
怎麼去解釋函數的執行機制呢?
我我的認爲,不少前端或者其餘編程語言的開發者,對計算機的一些底層原理不太清楚,好比編譯原理,計算機組成原理等。
我來模仿一個前端面試題:輸入一個 url
後,會發生什麼?(哈哈哈哈哈隔)。來出一個面試題:
執行一個函數,會發生什麼?
參考下面代碼:
function say() {
let str = 'hello world'
console.log(str)
}
複製代碼
是否是發現很酷,這道面試題要是交給你,你能答出多少呢?
中斷
5
分鐘想一下。
好了,中斷結束。若是讓我來答,我大體會這樣說:
首先我要建立一個函數,打住。若是你學過 C++
,你可能不會這樣說,你會這樣說,我要先開闢一個堆內存。
因此,我會從建立函數到執行函數以及其底層實現,這三個層次進行分析:
函數不是無緣無故產生的,你要去建立一個函數,而建立函數的時候,究竟發生了什麼呢?
答案以下:
第一步:我要開闢一個新的堆內存
爲何呢?由於每一個字母都是要存儲空間的,只要有數據,就必定得有存儲數據的地方。而計算機組成原理中,堆容許程序在運行時動態地申請某個大小的內存空間,因此你能夠在程序運行的時候,爲函數申請內存。
第二步:我建立一個函數
say
,把這個函數體中的代碼放在這個堆內存中。
想一下函數體是以什麼樣的形式放在堆內存中的?很明顯,是以字符串的形式。 爲何呢?咱們來看一下 say
函數體的代碼是什麼,以下:
let str = 'hello world'
console.log(str)
複製代碼
你以爲這些語句以什麼形式的結構放在堆內存中比較好呢,不用考慮也是字符串,由於沒有規律。若是是對象的話,因爲有規律,能夠按照鍵值對的形式存儲在堆內存中。而沒規律的一般都是變成字符串的形式。
第三步:在當前上下文中聲明
say
函數(變量),函數聲明和定義會提高到最前面
注意有個關鍵的點,就是當前上下文,咱們能夠理解爲上下文堆棧(棧),say
是放在堆棧(棧)中的,同時它的右邊還有一個堆內存地址,用來指向堆中的函數體的。
PS: 建議去學習一下數據結構,棧中的一塊一塊的,咱們稱爲幀。你能夠把棧理解中 DOM 樹,幀理解爲節點,每一幀( 節點 )都有本身的名字和內容。
第四步:把開闢的堆內存地址賦值給函數名
say
這裏一個關鍵點就是,把堆內存地址賦值給函數名 say
。
我特地在白板上畫了一個簡單的示意圖:
結合上圖 say
右邊的存儲,再去理解上面的四個步驟,是否是有點感悟了呢。
這裏我忽然想提一個簡單的知識,就是賦值這個操做,好比我把堆內存地址賦值給函數名 say
,那麼這意味着什麼呢?
有不少人可能不明白,其實這裏很簡單,這一步賦值操做,從計算機組成原理角度看,內存分爲好幾個區域,好比代碼區域,棧區域,堆區域等。
理解這幾個區域一個最關鍵的點,就是要明白,每個存儲空間的內存地址都是不同。也就是說,賦值(引用類型)的操做就是將堆區域的某一個地址,經過總線管道流入(複製)到對應棧區域的某一個地址中,從而使棧區域的某一個地址內的存儲空間中有了引用堆區域數據的地址,這裏業界叫句柄,說白了就是指針。只不過在高級語言中,把指針給隱藏了,直接有變量代替指針。
因此一個簡單的賦值,其在計算機底層實現上,都是很複雜的,這裏,也許經過彙編語言,你能夠更好的去理解賦值的真正含義,好比 1 + 1
用匯編語言編寫,就是下面代碼:
start:
mov ax, 1
mov bx, 1
add ax, bx
end start;
複製代碼
從上面代碼中,咱們能夠看到,把 1
賦值給 ax
,使用到了 mov
指令。而 mov
是 move
移動的縮寫,這也證實了,在賦值這個操做上,其實本質上是數據或者數據的句柄在一張地址表中的流動。
PS: 因此若是是值類型,那就是直接把數據,流(移動)到指定內存地址的存儲空間中。
建立函數就先說到這了,其實我已經說得很是詳細了,從計算機底層去解釋一些最基礎的現象。
執行函數這個步驟,也很是重要,執行函數到底是什麼樣的過程,如今我就用我我的的總結去解釋這個過程。
思考一個點。
咱們知道,函數體的代碼是在保存在堆內存中的,並且是字符串形式。那麼若是咱們要執行堆內存中的代碼,首先要作的就是講字符串變成真正的 JS
代碼,這個是比較容易理解的,就像數據傳輸中的序列化和反序列化。
思考題一:爲何會存在序列化和反序列化?你們能夠自行思考一下,有些越簡單的道理,背後越是有着非凡的思想
JS
代碼如何將字符串變成 JS
代碼,這裏有一個前置知識,就是:
每個函數調用,都會在函數上下文堆棧中建立幀。
棧是什麼?
棧是一個基本的數據結構,這裏我就不解釋了,小夥伴不懂的先去百度一下看看。
爲何函數執行要在棧中執行呢?
最關鍵的一點就是,棧是先進後出的數據結構,咱們想一下,被也就意味着能夠很好的保存和恢復調用現場。爲何?咱們來看一段代碼:
function f1() {
let b = 1;
function f2() {
cnsole.log(b)
}
return f2
}
let fun = f1()
fun()
複製代碼
這裏先不解釋,繼續往下看。
函數上下文堆棧是什麼?
咱們能夠這樣去理解,函數上下文堆棧是一個數據結構,無論它是什麼,若是學過 C++
或者 C
的,能夠理解成是一個 struct
(結構體)。這個結構體負責管理函數執行已經關閉變量做用域。函數上下文堆棧在程序運行時就會產生,而且一開始加入到棧裏面的是全局上下文幀,位於棧底。
首先要明白一點:
執行函數(函數調用)是在棧上完成的 。
這也就是爲何 JS
函數能夠遞歸。由於棧的先進後出的數據結構,賦予了其遞歸能力。
繼續往下看,函數執行大體有如下步驟:
第一步:會造成一個供代碼執行的環境,也是一個棧內存
這裏,咱們先思考幾個問題:
第二步:將存儲的字符串複製一份到新開闢的棧內存中,使其變爲真正的
JS
代碼
這步很好理解,
第三步:先對形參進行賦值,再進行變量提高,好比將
var
function
變量提高。
第四步:在這個新開闢的做用域中自上而下執行
思考題:爲何是自上而下執行呢?
將執行結果返回給當前調用的函數
思考題:將執行結果返回給當前調用的函數,其背後是如何實現的呢?
這裏爲何要談談底層實現呢,由於有還有一些知識點我沒有提到,好比前面的一些思考,這裏我想統一提一下。
函數在執行的時候,都會造成一個全新的私有做用域,也叫私有棧內存。
目的有以下幾點:
第一點:把原有堆內存中存儲的字符串變成真正的 JS
代碼
第二點: 保護該棧內存的私有變量不受外界的干擾
函數執行的這種保護機制,在計算機中稱之爲 閉包 。
可能有人不明白,咋就私有了呢?
沒問題,咱們能夠反推。假設不是私有棧內存的,那麼當你執行一個遞歸時,基本就完了,由於一個函數上下文堆棧中,有不少相同的 JS
代碼,好比局部變量等,若是不私有化,那豈不亂套了,因此假設矛盾,私有棧內存成立。
首先,你要明白 JS
的棧內存是系統自動分配的,大小固定。想想,若是自動適應的話,那就基本不存在除死循環這種狀況以外的的棧溢出了。
這個確實挺讓人好奇的,爲何呢?我舉個例子,你每天寫 return
語句,那你知道 return
的底層實現嗎?你每天都在寫子程序,那你知道子程序的底層的一些真相嗎?
咱們來看一張圖:
上圖顯示了一次函數調用的棧結構,從結構中咱們能夠看到,內部有哪些東西,好比實參,局部變量,返回地址。
看下面代碼:
function f1() {
return 'hello godkun'
}
let result = f1()
f2(result)
複製代碼
上面這行代碼的底層含義就是,f()
函數在私有棧內存中執行完後,使用 return
後,將 return
後的執行結果傳遞給 EAX
(累加寄存器),經常使用於函數返回值。對寄存器不瞭解的能夠自行搜索學習一下,這裏就再也不說了。這裏我說一下 Return Addr
,Addr
主要目的是讓子程序可以屢次被調用。
看下面代碼:
function main() {
say()
// TODO:
say()
}
複製代碼
上面代碼,在 main
函數中進行了屢次調用子程序 say
,在底層實現上面,是經過在棧結構中保存一個 Addr
用來保存函數的起始運行地址,當第一個 say
函數運行完之後,Addr
就會指向起始運行地址,以備後面屢次調用子程序。
JS
引擎是如何執行函數上面我從不少方面分析了函數執行的機制,可能有點難懂。如今我來簡要分析一下,JS
引擎是如何執行函數的。
這裏我就不造輪子了,有一篇博客寫的很是好,我發自心裏的認爲我寫不出來比這還好的博客了。就算寫出來,我感受也不必了。可是這篇博客寫的過於歸納,不少細節沒有提到,這裏我要在此篇博客的基礎上分析不少很重要的細節。
博客地址:
下面我開始分析,代碼以下:
//定義一個全局變量 x
var x = 1
function A(y) {
//定義一個局部變量 x
var x = 2
function B(z) {
//定義一個內部函數 B
console.log(x + y + z)
}
//返回函數B的引用
return B
}
//執行A,返回B
var C = A(1)
//執行函數B
C(1)
複製代碼
PS: 建議你們先看一下博客,知道一些基本概念,而後再看個人分析。
下面開始分析:
執行
A
函數時
JS
引擎構造的 ESCstack
結構以下:
簡稱 A
圖:
執行
B
函數時
JS
引擎構造的 ESCstack
結構以下:
簡稱 B
圖:
下面開始最爲關鍵的我的感悟 show time
。
核心看下面代碼:
EC(B) = {
[scope]:AO(A),
var AO(B) = {
z:1,
arguments:[],
this:window
},
scopeChain:<AO(B),B[[scope]]> } 複製代碼
這是在執行 B函數
時,建立的 B
函數的執行環境(一個對象結構)。裏面有一個 AO(B)
,這是 B
函數的活動對象。
那AO(B)
的目的是什麼?其實 AO(B)
就是每一個鏈表的節點其指向的內容。
同時,這裏還定義了 [scope]
屬性,咱們能夠理解爲指針,[scope]
指向了 AO(A)
,而 AO(A)
就是函數 A
的活動對象。
函數活動對象保存着 局部變量、參數數組、this
屬性。這也是爲何你能夠在函數內部使用 this
和 arguments
的緣由。
scopeChain
是做用域鏈,熟悉數據結構的同窗確定知道我想說什麼了,其實函數做用域鏈本質就是鏈表,執行哪一個函數,那鏈表就初始化爲哪一個函數的做用域,而後把當前指向的函數活動對象放到 scopeChain
鏈表的表頭中。
好比執行 B
函數,那 B
的鏈表看起來就是 AO(B) --> AO(A)
可是別忘了,A
函數也是有本身的鏈表的,爲 AO(A) --> VO(G)。因此整個鏈表就串起來來,B
的鏈表(做用域)就是:AO(B) --> AO(A) --> VO(G)
鏈表是一個閉環,由於查了一圈,回到本身的時候,若是還沒找到,那就返回 undefined
。
思考題:你們能夠思考一下 [scope] 和 [[scope]] 的命名方式,爲何是這種形式。
A
函數的 ECS
咱們能看到什麼咱們能看到,JS
語言是靜態做用域語言,在執行函數以前,整個程序的做用域鏈就同樣肯定好了,從 A
圖中的函數 B
的 B[[scope]]
就能夠看到做用域鏈已經肯定好了。不像 lisp
那種在運行時才能肯定做用域。
執行環境的數據結構是棧結構,其實本質上是給一個數組增長一些屬性和方法。
執行環境能夠用 ECStack
去表示,能夠理解成 ECSack = []
這種形式。
棧(執行環境)專門用來存放各類數據,好比最經典的就是保存函數執行時的各類子數據結構。
好比 A
函數的執行環境是 EC(A)
。當執行函數 A
的時候,至關於 ECStack.push[A]
,當屬於 A
的那些東西被放入到棧中的時候,都會被包裹成一個私有棧內存。
私有棧是怎麼造成的,這裏就要牽扯到彙編語言了,從彙編語言角度去看,會發現一個棧的內存分配,棧結構的各類變換,都是有底層標準去控制的。
因此咱們再聯繫一下,日常咱們所說的上下文環境啊,context
等,其實和我上面解釋的執行環境並無什麼區別,這樣去理解,是否是發現對上下文環境之類的專有名詞理解的更爲深入了呢。
由於再跳,本質仍是同樣的,計算機行業底層標準是肯定的。
this
this
爲何在運行時才能肯定
咱們看上面兩張圖中的紅色箭頭,箭頭處的信息很是很是重要。
看 A
圖,執行 A
函數時,只有 A
函數有 this
屬性,執行 B
函數時,只有 B
函數有 this
屬性,這也就證明了 this
只有在運行時纔會存在。
this
的指向真相
咱們看一下 this
的指向,A
函數調用的時候,屬性 this
的屬性是 window
,而 經過 var C = A(1)
調用 A
函數後,A
函數的執行環境已經 pop
出棧了。此時執行 C()
就是在執行 B
函數,EC(B)
已經在棧頂了,this
屬性值是 window
全局變量。
經過 A
圖 和 B
圖的比較,直接展現 this
的本質。看清真相,this
也不過如此。
聽不懂不要緊,聽我娓娓道來。
經過
A
圖 和B
圖的比較,直接秒殺 做用域 的全部用法
看 A
圖,執行 A
函數時,B
函數的做用域是建立 A
函數的活動對象 AO(A)
。做用域就是一個屬性,一個屬於 A函數的執行環境中的屬性,它的名字叫作 [scope]
。
[scope]
指向的是一個函數活動對象,其實這裏最核心的一點,就是你們要把這個函數對象當成一個做用域,但最好理解成一個鏈表節點。
若是你能理解成鏈表節點的話,那你就不會再對爲何會有做用域鏈這個東西感到陌生,不會再對做用域和做用域鏈的區別而感到困惑。直接秒殺了做用域相關的全部問題。
PS:
B
執行B
函數時,只有B
函數有this
屬性,這也就交叉證明了this
只有在運行時纔會存在。
首先經過比較 A
圖和 B
圖的 scopeChain
,咱們能夠肯定的是:
做用域鏈本質就是鏈表,執行哪一個函數,那鏈表就初始化爲哪一個函數的做用域,而後將該函數的 [scope]
放在表頭,造成閉環鏈表,做用域鏈的查找,就是經過鏈表查找的,若是走了一圈還沒找到,那就返回 undefined
。
做用域鏈也是很 easy
的。
我決定再舉一個例子,這是一個常常被問的面試題,看下面代碼:
第一個程序以下:
function kun() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i
}
}
return result
}
let r = kun()
r.forEach(fn => {
console.log('fn',fn())
})
複製代碼
第二個程序以下:
function kun() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = (function(n) {
return function() {
return n
}
})(i)
}
return result
}
let r = kun()
r.forEach(fn => {
console.log('fn', fn())
})
複製代碼
上面兩個程序會輸出什麼結果?並分析一下其原理。
輸出結果你們應該都知道了,結果分別是以下截圖:
第一個程序,輸出 10
個 10
:
第二個程序,輸出 0
到 9
:
那麼問題來了,其內部的原理機制你知道嗎?
coder
只能答到當即調用,閉包。coder
能夠答到做用域相關知識。coder
(大佬級別) 能夠從核心底層緣由來分析。下面我來展現一下從核心底層緣由來分析,是一種什麼樣的 style
。
代碼以下:
function kun() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i
}
}
return result
}
let r = kun()
r.forEach(fn => {
console.log('fn',fn())
})
複製代碼
如何去分析,首先咱們要明白一點,只有函數在執行的時候,函數的執行環境纔會生成。那依據這個規則,咱們能夠知道在完成 r = kun()
的時候,kun
函數只執行了一次,生成了對應的 AO(kun)
。咱們能夠看一下 AO(kun)
有什麼,以下:
AO(kun):{
i = 10;
kun = function(){...};
kun[[scope]] = this;
}
複製代碼
這時,在執行 kun()
以後,i
的值已是 10
了。OK
,下面最關鍵的一點要來了,請注意,kun
函數只執行了一次,也就意味着:
在 kun
函數的 AO(kun)
中的 i
屬性是 10
。
咱們繼續分析, kun
函數的做用域鏈以下:
AO(kun)
--> VO(G)
並且 kun
函數已經從棧頂被刪除了,之只留下了 AO(kun)
,注意一點:
這裏的 AO(kun)
表示一個節點,這個節點有指針和數據,其中指針指向了 VO(G)
,數據就是 kun
函數的活動對象。
因此下面問題來了,當去一次執行 result
中的數組的時候,會發生什麼現象?注意一點:
result
數組中的每個函數其做用域都已經肯定了,上面也提到過,JS
是靜態做用域語言,其在程序聲明階段,全部的做用域都將肯定。
因此知道這點之後,那麼 result
數組中每個函數其做用域鏈都是以下:
AO(result[i]) --> AO(kun) --> VO(G)
複製代碼
所以 result
中的每個函數執行時,其 i
的值都是沿着這條做用域鏈去查找的,並且因爲 kun
函數只執行了一次,致使了 i
值是最後的結果,也就是 10
。因此輸出結果就是 10
個 10
。
總結一下,就是 result
數組中的 10
個函數在聲明後,總共擁有了 10
個鏈表(做用域鏈),都是 AO(result[i]) --> AO(kun) --> VO(G)
這種形式,可是 10
個做用域鏈中的 AO(kun)
都是同樣的。因此致使了,輸出結果是 10
個 10
。
經過上面的解釋,其實已經從一個至關底層的視角去分析了,須要注意的關鍵點,我也都提了出來,你們再好好研究下吧。
下面咱們來分析輸出 0
到 9
的結果。
代碼以下:
function kun() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = (function(n) {
return function() {
return n
}
})(i)
}
return result
}
let r = kun()
r.forEach(fn => {
console.log('fn', fn())
})
複製代碼
經過分析 輸出結果爲 10
個 10
的狀況,你們應該有所收穫了,或者找到一些感受了,那輸出 0
到 9
結果的狀況,該怎麼去分析呢?且聽我娓娓道來。
首先和上面不同了,在聲明函數 kun
的時候,就已經執行了 10
次匿名函數了。還記得只要執行函數,就會生成函數執行環境麼。也就意味着,在 ECS
棧中,有一個 EC(kun)
執行環境,可是有10個匿名的 EC(匿名)
執行環境,分別對應的是 result
數組中的 10
個函數。
具體展現狀況,我來用僞代碼表達一下:
下面是執行 kun
函數的時候。
ECSack = [
EC(kun) = {
[scope]: VO(G)
AO(匿名1) = {
i: 0,
result[0] = function() {...// return i},
arguments:[],
this: window
},
scopeChain:<AO(kun), kun[[scope]]>
},
// .....
EC() = [
[scope]: AO(kun)
AO(kun) = {
i: 9,
result[9] = function() {...// return i},
arguments:[],
this: window
},
scopeChain:<AO(kun), kun[[scope]]>
]
]
複製代碼
上面簡單的用結構化的語言表示了 kun
函數在聲明時的內部狀況,首先有兩點要注意。
第一點:每個 EC(kun)
中的 AO(kun)
中的 i
屬性值都是不同的,好比經過上面結構化表示,能夠看到:
result[0]
函數的父執行環境是 EC(kun)
,這個 VO(kun)
裏面的 i
值 是 0
。result[9]
函數的父執行環境是 EC(kun)
,這個 VO(kun)
裏面的 i
值 是 9
。第二點:關於做用域鏈,也就是 scopeChain
,result
中的函數的 鏈表形式仍然是下面這種形式
AO(result[i]) --> AO(kun) --> VO(G)
複製代碼
但不同的是,對應節點的存儲地址不同了,至關因而 10
個新的 AO(kun)
。而每個 AO(kun)
的節點內容中的 i
值是不同的。
因此總結下就是:
執行 result
數組中的 10
個函數時,走了 10
個不一樣的鏈表,同時每一個鏈表的 AO(kun)
節點是不同的。每一個 AO(kun)
節點中的 i
值也是不同的。
因此輸出的結果最後顯示爲 0
到 9
。
是否是發現從底層去分析和理解的話,不少問題其實都有一個很合理,或者讓閱讀者能夠接受的答案。
敲山震虎篇的知識難度有點大,費了我很多腦子,經過對底層實現原理的分析,咱們能夠更加深入的去理解函數的執行機制。
深入的理解了函數的執行機制,咱們才能更流暢的寫出高質量的函數。
如何減小做用域鏈(鏈表)的查找
好比咱們看不少庫,想 JQ
等,都會在當即執行函數的最外面傳一個 window
參數。這樣作的目的是由於,window
是全局對象,經過傳參,避免了查找整個做用域鏈。提升了函數的執行效率,看法了寫出了高質量的函數。
如何防止棧溢出
咱們知道,每一次執行函數,都會建立函數的執行環境,也就意味着佔用一些棧內存,而棧內存大小是固定的,若是寫了很大的遞歸函數,大家就會形成棧內存溢出,引起錯誤。
我以爲,咱們要去努力的達成這樣一個成就:
作到當我在手寫一個函數時,我心中很是清楚的知道我正在寫的每一行代碼,其在內存中是怎麼表現的,或者說其在底層是如何執行的,從而達到 眼中有碼,心中無碼 的境界。
若是能作到這樣的話,那還怕寫不出高質量的函數嗎?
後續會有其餘兩篇博客,分別是基礎篇和高級篇,能夠關注個人掘金博客或者 github
來獲取後續的系列文章更新通知。
掘金系列技術文章彙總以下,以爲不錯的話,點個 star 鼓勵一下。
我是源碼終結者,歡迎技術交流。
也能夠進 前端狂想錄羣 你們一塊兒頭腦風暴。有想加的,由於人滿了,能夠先加我好友,我來邀請你進羣。
今天是一個開心的節日,既是吃湯圓猜燈謎的元宵節,也是程序員通宵的節日(猿宵節)。
雖然 20
號了,但啥也別說了,祝各位首富來年元宵節快樂(嘿嘿)。
最後:尊重原創,轉載請註明出處哈😋