godkun/blog

如何编写高质量的函数 -- 敲山震虎的答疑篇

godkun opened this issue · 0 comments

引言

针对前面我写的

如何编写高质量的函数 -- 敲山震虎篇

的评论区,小伙伴提出的一些问题在这里做一个统一的回答。

当然,鼓励支持的评论就不说了,谢谢小伙伴的鼓励支持,但是对于一些评论和私聊的小伙伴的疑问,我在这里要统一回复下。

PS: 提一下一些公众号(掘金,奇舞周刊等吧)转载的情况,由于文章一开始有错误,然后公众号又没有同步,然后我又不能帮你们改,于是点开公众号文章,看着那些还在的错误,却无能为力的心情,心里默默 ob : 那就这样吧。

问题一 [书籍推荐]

问题描述

膜拜大佬啊,请问有推荐的书籍学习吗?

问题解决

可以看看我写的这篇文章:

新时代下前端工程师的推荐书籍和必备知识

这算是目前我认为最具有前瞻性的前端推荐书籍系列的文章(不吹不黑)。

问题二 [**活动太多]

问题描述

感觉个人的**活动不要太多,快速切入主题。

解决答案

看到后,我把前面的很多**活动都注释掉了,只能在编辑中的注释中看到了。

如图所示:

PS: 这里的**活动,主要原因是这是第一篇文章,后面的就不会啰嗦了。

问题三 [读起来很烧脑]

问题描述

这种难度的写成流程图就好了,现在这样看起来好烧脑。

解决答案

说的对,流程图也画过,但就跟 PPT 一样,没有感觉,画出来感觉很丑,而且很费事。看到网上那些很酷的流程图,我表示莫名的好奇和羡慕,这里问一下有没有这方面有经验的博主或者其他小伙伴,可以推荐一下,怎么快速画流程图。

PS:有没有那种高效率,效果有很酷,比如用触控笔?或者自动化和集成度更高的那种。

问题四 [敲山震虎什么鬼哦]

问题描述

作者是如何理解和使用 敲山震虎 这个成语的

解决答案

嗯哼?作者可是在公司十次叫别人的名字,5 次都会叫错的人。我怎么可能理解 敲山震虎 成语的意识,不存的,唯一能解释通的就是,我在起标题时,突然想到了这个成语,突然觉得很酷,然后就用了。

当然了,评论区已经有小伙伴给我取好标题了:

《编写高质量的函数(1)--了解函数底层知识》

问题五 [只有分析,没有怎么编写]

问题描述

描述一

写出高质量的函数,怎么写才是高质量呢,感觉内容有点跑题,中心要表达的不明确。

描述二

讲的是执行原理,不是怎么写好。

描述三

你说到函数创建,我第一反应是解释器翻译成抽象语法树再弄成机器码。太长了,编写高质量的函数的文章后面怎么又出现了老生常谈的 1010 闭包问题。

解决答案

确实大部分内容都是在分析函数的执行机制,这点没错,看起来好像跑题了,但这也和本身标题起的有关系,总结一下确实有点跑题。但是,我还想说一些事情。

本篇文章全程说执行原理的目的是什么?

是想让大家拥有这种能力:

在敲下函数的每一行代码时,或者去阅读每一个别人写的函数时,都能构很自然的体会到其底层的一些你看不到的过程和真相。古人云,只可意会,不可言传。这是内心深处的一种领悟吧。

但是我还是决定在答疑篇,依旧举文章中,最后一个例子来说一下,大家应该怎么想这个事情。

文章最后的那个面试题例子,其实就是在说明你在看一个函数和在写一个函数时,你需要注意什么。

如果你想让 i 都是 10 ,那你就让 result 中的函数的父执行环境引用同一个 [scope] 属性,说白了也就是让 result[i] 函数拥有相同的父执行环境。如果你想让 i09 ,那你就让 result[i] 函数拥有不同的父执行环境。怎么做到不同呢?最简单的方法就是在外层套一个匿名的立即执行函数,使其成为 result[i] 的父执行环境。同时这样嵌套以后,你还应该要想到一个事情,那就是你要把 不同的 i 存到 AO(匿名函数) 这样你才能保证输出 010

看到这,你就应该想到一个事情,就是为什么使用 let 就不用这样了呢?

因为 var 存在变量提升,变量提升是什么?变量提升直白点说,就是把变量的作用域提升了。本来应该在块级作用域中,但是由于原来的 JS 不存在块级作用域,那就只能提升到函数作用域了,所以执行子函数时,大家都去上一层作用域找了,这时显而易见都是 10

let 使块级作用域变成了可能,从而就不再需要在最外层套一个函数来特意的形成一个函数作用域。

PS:可以思考一下,let 的底层实现,这里我给点看法,有两种可能。第一种可能,如果按照语法糖的角度来说,let 一定是通过函数作用域来实现了局部作用域。如果按照非语法糖的角度来说,那 let 就是通过在 JS 底层级别,进行了相应改动,相关资料可以看 262

让你对这些东西越来越有感觉的时候,你就自然的知道如何去看待一个函数。如何去写出一个高质量的函数,你写的时候你会问自己为什么要按照这种所谓的 best practice 去写。所以我就回答这里吧,后续有啥疑问可以继续提出来,一起讨论也是一件开心的事情。

PS:最后关于描述三,我要说一下。解释器翻译成 ast 再变成机器码这个过程,我认为是适合于所有的创建的,在我这篇文章的函数创建部分,说这个有点不好,过于宽泛。

问题六 [ Lexical Environment ]

问题描述

Activation object 这个概念的 ES3 里的, 打 ES5 开始就没这个说法了, 取而代之的是使用 Lexical Environment 来描述作用域的。作者能讲讲这个吗?

解决答案

这里我就不讲了吧(头发少掉点...),换汤不换药系列😂。有想深入了解的,可以点击下面连接,传输到 ecma262 对应的 issues

https://github.com/tc39/ecma262/issues?utf8=%E2%9C%93&q=Lexical+environment

有啥别致的收获也可以加我好友和我分享一下。

问题七 [关于 this ]

问题描述

this 指向那个,还是没有看明白 this 指向的判定原则,例如对象函数调用的时候,this 指向那个对象本身的时候利用这篇文章的知识要怎么解释呢?谢谢了

解决答案

OK ,这个问题问的好,首先你可以看一下下面这篇文章

JavaScript深入之从ECMAScript规范解读this

群里小伙伴分享的,我大致看了下,分析的非常透彻,如果你认真看,可以解决你的问题,从规范层面上做了解释。

我感觉我自己怎么在造轮子哈哈哈(另一个我赶紧针扎了一下,造轮子不存在的,必须有点特色)。

那下面我来分析一下吧,用一种意识流来帮你分析一下。

提问者说的是:对象函数调用的时候,this 是怎么玩的,比如下面代码:

let a = 1
const kun = {
  a: 2,
  say: function () {
    return this.a;
  }
}

function Kun() {
  this.a = 33
  this.say = function () {
    return this.a;
  }
}

// Kun.prototype.a = 33
// Kun.prototype.say = function () {
//   return this.a;
// }

console.log(kun.say())
console.log((false || kun.say)())

let o = new Kun()
console.log(o.say())
console.log((false || o.say)())

输出结果如下:

对应红线表示,对应的输出结果,至于第二种为什么是 undefined , 按照上面我给的文章链接,解释如下图:

具体原因,后续再说吧。

我想说的是如下我个人的看法:

你看看上面的代码,使用字面量和使用构造函数的结果都是一样的。

想到这,我们是不是应该换个思维去想一下,是不是可以把 const kun = {} 可以当成 new Kun() 来看待。比如人们常说 const kun = {} 就是一个最经典的单例。

下面我先进行,第一种方式的推导:

按照 new Kun() 的过程思考

当执行完 kun = {...} 的时候,如果按照 new 创建对象的方式,来去分析的话,

会经历下面几个过程:

第一:执行构造函数,形成一个私有栈内存(作用域)

第二:将堆内存中的代码复制到栈内存中

第三:各种变量 change

第四:开辟一个堆内存

第五:将 AO (构造函数)中的 this 指向这个堆内存

第六:进行对象的各种属性和方法的初始化,比如增加 a 属性和 say 方法

最后:new 完后,把堆内存地址返回给变量,比如 kun ,然后从这里开始,kun 这个变量就在保留在栈中,并且指向上面的堆内存。

这个时候当你去执行 kun.say() 的时候,就是把 say 函数放到栈顶进行执行,那么问题来了,AO(say) 中的 this 指向什么?按照我写的博客中的说话,是指向调用者,就像全局函数的调用者是 window 对象,这里 say 的调用者是 kun 对象。

问题是这一步究竟怎么理解呢?

如果按照我写的博客去理解的话,你可以理解成当执行完构造函数 Kun 时,会留下一个 AO(Kun) 当然也就是一个节点,AO(Kun) 中的 this 指向了 kun 变量指向的堆内存。所以当 kun.say 执行时,里面的this会向上查找,当找到AO(Kun)的时候,会找到this,也就是变量kun指向的那一块堆内存。

这也借鉴了从根本上解释了,为什么单例模式要比各种 new 的性能要好了,因为单例模式,只有一个 AO(单例构造函数)。一个也就意味着只有一个堆内存。因为只有一个 this

我觉得我推的应该够清晰了,也可能是胡诌的比较有道理,你再思考思考吧,这个答案先这么回答了😂。

下面进行第二种推的方式:

这里第二种的方式后面再说,先按上面去理解吧。

PS: 所以我就不写面试相关的文章,这类文章太多了,我已经超越不了他们了😂-->

问题八 [缺少匿名函数作用域]

问题描述

描述一

我有一个问题,在解释第二种为什么输出是 0-9 的时候,为什么作用域链 没有匿名函数的作用域节点。并且还是和之前的那种一样即:AO(result[i]) --> AO(kun) --> VO(G) ,匿名函数应该也是有自己的 [scope] 的啊。为什么不是 AO(result[i]) --> AO(匿名i) -->AO(kun) --> VO(G) 。

描述二

执行 result[i] 函数中 return i 时,查找的作用域链都是 AO(result[i]) --> AO(kun) --> VO(G) ,输出 10 的原因,是 result[i] 的作用域中没有 i 变量,而去寻找的上一作用域节点 AO(kun) 。立即执行函数,给 result[i] 增加一层作用域,0-9 函数 AO(kun)i: 10

解决答案

这里你们说的对,作用域链要有匿名函数的作用域节点。同时 i 也不是在 VO(G) 中的,是在 AO(匿名函数) 中。

问题九 [C/C++建议]

问题描述

作者码这么多字真心不容易哇,看评论区一片叫好的,说几点觉得不同意的地方或建议

  1. 可以把堆栈解释的更清楚些,堆栈既可以表示数据结构,也可以指代内存。
  2. js 层面只有调用栈(数据结构为栈,在 C++ 层面分配在堆内存中)溢出, Maximum call stack size exceeded ,控制台下只有栈内存溢出。
  3. 总感觉在拿 C/C++ 的一套东西来解释 js ,有些不合适。

解决答案

建议很好,这些地方笔者确实没有注意到,比如第二点,我确实不知道这个点,谢谢你的建议,学习了一波,后续我会研究一下这方面的知识的。

问题十 [执行过程分析不正确]

问题描述

描述一

没明白第二个输出 0-9 的例子中首先和上面不一样了,在声明函数 kun 的时候,就已经执行了 10 次匿名函数了 这句话。声明阶段也会运行 kun 内部的代码?不是执行阶段的时候?我还以为是 ECStack[EC(匿名0)EC(kun)EC(G) ],然后 EC(匿名0) 出栈,EC(匿名1)进栈一直到9出栈,然后EC(kun)` 出栈。专门为此注册了账号,希望作者大大给小白解释一下

描述二

for 循环给数组中每一项放入了一个匿名自调函数,这个匿名自掉函数返回了一个函数,这样数组中的每一项都是一个函数的引用。forEach 的时候调用的其实是匿名函数返回的函数,其实就是闭包。

问题解答

这里文章中的写法有点问题,正确的是执行函数 kun 的时候,执行了 10 次匿名函数。

评论的小伙伴的链接推荐

leto 小伙伴

关于栈帧的文章推荐下面链接,讲解的非常透彻

https://www.cnblogs.com/clover-toeic/p/3755401.html

xbup 小伙伴

可以去看汤姆大叔的文章。系列文章对 函数执行时的 变量对象,活动对象,作用域链等有很详细的讲解。

这里我把汤姆博客地址贴一下

https://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html

看截图我知道,又一个美好的故事要发生了。

风之语

深夜 4 点完成的博客,发了出来,发完第二天除了回复评论,文章内容我都没看,后面开始看了下文章内容,确实有一些逻辑错误,谢谢小伙伴的纠正。