内容简介:一千个读者,有一千个哈姆雷特。我将会从函数的执行机制、鲁棒性、函数式编程、设计模式等方面,全面阐述如何编写高质量的函数。如何编写高质量的函数,这是一个很难回答的问题,不同人心中对高质量有自己的看法,这里我将全面的阐述我个人对如何编写高质量函数的一些看法。看法可能不够全面,也可能会有一些错误的见解,欢迎一起讨论,就像过日子的人,小吵小闹总会不经意的出现,一颗包容的心莫过于是最好的
一千个读者,有一千个哈姆雷特。
我将会从函数的执行机制、鲁棒性、函数式编程、 设计模式 等方面,全面阐述如何编写高质量的函数。
序
如何编写高质量的函数,这是一个很难回答的问题,不同人心中对高质量有自己的看法,这里我将全面的阐述我个人对如何编写高质量函数的一些看法。看法可能不够全面,也可能会有一些错误的见解,欢迎一起讨论,就像过日子的人,小吵小闹总会不经意的出现,一颗包容的心莫过于是最好的 best practice
。
现实很骨感
有些时候,可爱的你可能会有这样的疑问,我也知道要编写高质量的函数,我也知道编写高质量函数的基本方法,但是我在工作中就是没法付出实践,一而再,再而三的没法付出实践。
像这种知道怎么做,但是无法付出实践的事情,其实不是你个人的问题,这是个通病,几乎 90%
的人都会遇到,那么原因是什么呢?
我个人认为,这需要从两个层次去理解,为什么现实很骨感。
接手旧业务
首先,这里的旧业务是指:你要在别人完成好的某个需求的代码基础上进行开发。这几乎是所有 coder
都会遇到的情况。每个人可能都会在工作中吐槽一句,这写的什么玩意?之类的吐槽话。很正常,说不定你写的代码,此刻也正在被别的 coder
吐槽呢。
为什么旧业务的现实很骨感,原因大致如下:
200 coding
接手新业务
能接新业务,算是很不错的了,从业务代码的初始化到完成上线,都是你全程参与,既可以锻炼你的工程化思想,也可以实践一下你写的代码在日后能不能轻松的扩展。少被别的 coder
说坑。
但是就算是在做新业务的情况下,现实有时也很骨感,原因大致如下:
coder 7
总结
上面的是我个人的看法,但是八九不离十。很多情况下,不是不会,努力努力都会的,但是有时候啊,你会发现,总会有一部分人在尽可能的去尝试,然后你和别人的差距就出来了。可以这么说,这里的每一个理由,都可以抽象成一个需求,而你要做的就是去努力解决这个需求。加油吧少年,人与人的差距就是这样产生的。
幕后花絮:事实是,我的一些业务(不开心的业务等因素)代码,也很一般,我同事看到此文别笑,都是搬砖工(码农),何苦为难搬砖工(码农)。
本文写作风格
上面我又胡诌了一些东西,控制不住,本人一副要准备写长篇小说的样子(不要脸),因为我觉得,我的博客不能太官方,不然我自己都不想看。写博客趋向意识流(胡诌流),按着我心里想的去写,不会去详细的说明某一个知识点,如果需要详细讨论,可以在文末加我微信细聊,或者扫码进微信群大家一起头脑风暴。
微信二维码如下:
欢迎小伙伴入群一起讨论技术,一起进步,群很活跃,技术氛围不错。
好了,胡诌完了,下面开始吧,我打算用三篇文章来完成 如何编写高质量的函数 这个系列。
三篇文章我将从以下几个方面去阐述,如何编写高质量的函数。
- 函数(一切皆有可能)
- 函数的命名
- 函数的注释
- 函数的复杂度
- 函数的鲁棒性(防御性编程)
- 函数的入参和出参(返回)
- 如何用函数式编程打通函数的任督二脉
- 如何用设计模式让函数如虎添翼
- 编写对
V8
友好的函数是一种什么style
- 前端工程师的函数狂想录
下面开始胡诌啦:小声 BB
,可以先点个赞鼓励一下么(给一波精神上的鼓励)。
PS: 写过文章的小伙伴都知道,写一篇好文很不容易,很消耗精力,而写完博客后,最大的精神满足莫过于小伙伴的一个肯定的赞。 哎,其实这个系列已经写好了,本来想一篇文章发完的,但是看了下, 2
万字,太多了,还是分三篇吧。
本篇只说第一节 函数
,擒贼先擒王,下面我们来盘一盘函数的七七八八,往 XXX
上盘:joy:。
函数(一切皆有可能)
函数二字,代表着一切皆有可能。
我们想一下:我们用的函数究竟离我们有多远。就像打麻将一样,你觉得你能像雀神那样,想摸啥就来啥么(夸张修辞手法)。
天天和函数打交道,有没有想过函数出现的目的是什么?我们再深入想一下,函数的执行机制是什么?下面我们就来简单的分析一下。
函数出现的目的
函数是迄今为止发明出来的用于节约空间和提高性能的最重要的手段。
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
那种在运行时才能确定作用域。
执行环境是一种什么样的存在
其实可以把执行环境是看出是一个对象结构,比如 A
函数的执行环境是 EC(A)
有 [scope]
属性
开启上帝模式看穿 this
this
为什么在运行时才能确定
我们看上面两张图中的红色箭头,箭头处的信息非常非常重要。
看 A
图,执行 A
函数时,只有 A
函数有 this
属性,执行 B
函数时,只有 B
函数有 this
属性,这也就证实了 this
只有在运行时才会存在。
通过 A
图 和 B
图的比较,直接展示 this
的本质。看清真相, this
也不过如此。
this
的指向真相
我们看一下 this
的指向, A
函数调用的时候,属性 this
的属性是 window
,而 通过 var C = A(1)
调用 A
函数后, A
函数的执行环境已经 pop
出栈了。此时执行 C()
就是在执行 B
函数, EC(B)
已经在栈顶了, this
属性值是 window
全局变量。
作用域
通过 A
图 和 B
图的比较,直接秒杀 作用域 的所有用法
看 A
图,执行 A
函数时, B
函数的作用域是创建 A
函数的活动对象,同时也是 B
执行 B
函数时,只有 B
函数有 this
属性,这也就证实了 this
只有在运行时才会存在。
我们显示的看到了函数的作用域是怎么形成的,也看
通过 A
图 和 B
图的比较,直接秒杀 作用域链 的知识
首先我们可以确定的是,作用域链本质就是链表,执行哪个函数,那链表就初始化为哪个函数的作用域,然后将。
用一道面试题让你更上一层楼(走火入魔)
我决定再举一个例子,这是一个经常被问的面试题,看下面代码:
第一个程序如下:
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
。
分析输出10个10
代码如下:
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
函数只执行了一次,生成了对应的 VO(kun)
和 AO(kun)
。我们可以看一下 VO(kun)
有什么,如下:
VO(G):{ i = 10; kun = function(){...}; kun[[scope]] = this; } 复制代码
这时,在执行 kun()
之后, i
的值已经是 10
了。 OK
,下面最关键的一点要来了,请注意, kun
函数只执行了一次,也就意味着:
在 kun
函数的 scopeChain
也就是链表(作用域链)里的 VO(G)
中的 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)
和 VO(G)
都是一样的。所以导致了,输出结果是 10
个 10
。
通过上面的解释,其实已经从一个相当底层的视角去分析了,需要注意的关键点,我也都提了出来,大家再好好研究下吧。
下面我们来分析输出 0
到 9
的结果。
分析输出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
栈中,有 10
个 EC(kun)
执行环境,分别对应的是 result
数组中的 10
个函数。
具体展示情况,我来用伪代码表达一下:
ECSack = [ EC(kun) = { [scope]: VO(G) AO(kun) = { result[0] = function() {...}, arguments:[], this: window }, scopeChain:<AO(kun), kun[[scope]]> }, // ..... EC(kun) = [ [scope]: VO(G) AO(kun) = { result[9] = function() {...}, arguments:[], this: window }, scopeChain:<AO(kun), kun[[scope]]> ] ] 复制代码
上面简单的用结构化的语言表示了 kun
函数在声明时的内部情况,首先有两点要注意。
第一点:每一个 EC(kun)
中的 VO(G)
都是不一样的,比如通过上面结构化表示,可以看到, result[0]
函数的父执行环境的 [scope]
是 VO(G)
,同时这个 VO(G)
里面的 i
是 0
。记住 VO(G)
是一段存储空间,每个 EC(kun)
的 VO
都是不一样的。
第二点:关于作用域链,也就是 scopeChain
, 10
函数,链表形式仍然是下面这种形式
AO(result[i]) --> AO(kun) --> VO(G) 复制代码
但不一样的是,对应节点的存储地址不一样了,相当于是 10
个新的 AO(kun)
。而每一个 AO(kun)
的节点内容中的 i
值是不一样的, i
是存的当时立即执行时, VO(G)
中的 i
的值。
所以总结下就是:
执行 result
数组中的 10
个函数时,走了 10
个不同的链表,同时每个链表的 AO(kun)
节点是不一样的。每个 AO(kun)
节点中的 i
值也是不一样的。
所以输出的结果最后显示为 0
到 9
。
总结
是不是发现从底层去分析和理解的话,很多问题其实都有一个很合理,或者让阅读者可以接受的答案。
总结
敲山震虎篇的知识难度有点大,费了我不少脑子,通过对底层实现原理的分析,我们可以更加深刻的去理解函数的执行机制。
深刻的理解了函数的执行机制,我们才能更流畅的写出高质量的函数。
如何减少作用域链(链表)的查找
比如我们看很多库,想 JQ
等,都会在立即执行函数的最外面传一个 window
参数。这样做的目的是因为, window
是全局对象,通过传参,避免了查找整个作用域链。提高了函数的执行效率,见解了写出了高质量的函数。
如何防止栈溢出 我们知道,每一次执行函数,都会创建函数的执行环境,也就意味着占用一些栈内存,而栈内存大小是固定的,如果写了很大的递归函数,你们就会造成栈内存溢出,引发错误。
我觉得,我们要去努力的达成这样一个成就:
做到当我在手写一个函数时,我心中非常清楚的知道我正在写的每一行代码,其在内存中是怎么表现的,或者说其在底层是如何执行的,从而达到 眼中有码,心中无码 的境界。
如果能做到这样的话,那还怕写不出高质量的函数吗?
备注
- 敲山震虎篇阅读难度有点大,多去分析分析就会明白了
- 文章难免有错,还请多多包涵,欢迎在评论处指出错误,多多交流
参考
交流
后续会有其他两篇博客,分别是基础篇和高级篇,可以关注我的掘金博客或者 github
来获取后续的系列文章更新通知。
掘金系列技术文章汇总如下,觉得不错的话,点个 star 鼓励一下。
我是源码终结者,欢迎技术交流。
风之语
今天是一个开心的节日,既是吃汤圆猜灯谜的元宵节,也是 程序员 通宵的节日(猿宵节)。
虽然 20
号了,但啥也别说了,祝各位首富来年元宵节快乐(嘿嘿)。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 如何通过 JavaScript 编写高质量的函数(三):函数式编程之理论篇
- 如何通过 JavaScript 编写高质量的函数(四):函数式编程之实战篇
- 如何编写高质量的函数 -- 命名/注释/鲁棒篇
- 编写高质量箭头函数的5个最佳做法
- [译] 编写高质量箭头函数的 5 个最佳做法
- 对高质量数据的追求
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
图片转BASE64编码
在线图片转Base64编码工具
RGB CMYK 转换工具
RGB CMYK 互转工具