主存包括随机存储器RAM和只读存储器ROM,其中ROM又可以分为MROM(一次性)、PROM、EPROM、EEPROM。ROM中存储的程序(例如启动程序、固化程序)和数据(例如常量数据)在断电后不会丢失。RAM主要分为静态RAM(SRAM)和动态RAM(DRAM)两种类型(DRAM种类很多,包括SDRAM、RDRAM、CDRAM等),断电后数据会丢失,主要用于存储临时程序或者临时变量数据。DRAM一般访问速度相对较慢。由于现代CPU读取速度要求相对较高,因此在CPU内核中都会设计L1、L2以及L3级别的多级高速缓存,这些缓存基本是由SRAM构成,一般访问速度较快。
上图右侧主存中的指令是CPU可以支持的处理命令,一般包含算术指令(加和减)、逻辑指令(与、或和非)、数据指令(移动、输入、删除、加载和存储)、流程控制指令以及程序结束指令等,由于CPU只能识别二进制码,因此指令是由二进制码组成。除此之外,指令的集合称为指令集(例如汇编语言就是指令集的一种表现形式),常见的指令集有精简指令集(ARM)和复杂指令集(InterX86)。一般指令集决定了CPU处理器的硬件架构,规定了处理器的相应操作。
温馨提示:感兴趣的同学可以了解一下ARM芯片的程序运行原理,包括使用IDE进行程序的编译(IDE内置编译器,主流编译器包含ARMCC、IAR以及GCCFORARM等,其中一些编译器仅仅随着IDE进行捆绑发布,不提供独立使用的能力,而一些编译器则随着IDE进行发布的同时,还提供命令行接口的独立使用方式)、通过串口进行程序下载(下载到芯片的代码区初始启动地址映射的存储空间地址)、启动的存储空间地址映射(包括系统存储器、闪存FLASH、内置SRAM等)、芯片的程序启动模式引脚BOOT的设置(例如调试代码时常常选择内置SRAM、真正程序运行的时候选择闪存FLASH)等。
如果某种高级语言或者应用语言(例如用于人工智能的计算机设计语言)转换的目标语言不是特定计算机的汇编语言,而是面向另一种高级程序语言(很多研究性的编译器将C作为目标语言),那么还需要将目标高级程序语言再进行一次额外的编译才能得到最终的目标程序,这种编译器可称为源到源的转换器。
除此之外,有些程序设计语言将编译的过程和最终转换成目标程序进行执行的过程混合在一起,这种语言转换程序通常被称为解释器,主要作用是将某种语言编写的源程序作为输入,将该源程序执行的结果作为输出,解释器的作用如下图所示:
解释器和编译器有很多相似之处,都需要对源程序进行分析,并转换成目标机器可识别的机器语言进行执行。只是解释器是在转换源程序的同时立马执行对应的机器语言(转换和执行的过程不分离),而编译器得先把源程序全部转换成机器语言并产生目标文件,然后将目标文件写入相应的程序存储器进行执行(转换和执行的过程分离)。例如Perl、Scheme、APL使用解释器进行转换,C、C++则使用编译器进行转换,而Java和JavaScript的转换既包含了编译过程,也包含了解释过程。
JavaScript中的数组存储大致需要分为两种情况:
温馨提示:可以想象一下连续的内存空间只需要根据索引(指针)直接计算存储位置即可。如果是哈希映射那么首先需要计算索引值,然后如果索引值有冲突的场景下还需要进行二次查找(需要知道哈希的存储方式)。
编译器的设计是一个非常庞大和复杂的软件系统设计,在真正设计的时候需要解决两个相对重要的问题:
阅读链接:基于Vue实现一个简易MVVM[5]-观察者模式和发布/订阅模式
编程范式(Programmingparadigm)是指计算机编程的基本风格或者典型模式,可以简单理解为编程学科中实践出来的具有哲学和理论依据的一些经典原型。常见的编程范式有:
阅读链接::如果你对于编程范式的定义相对模糊,可以继续阅读Whatistheprecisedefinitionofprogrammingparadigm\[6]了解更多。
不同的语言可以支持多种不同的编程范式,例如C语言支持POP范式,C++和Java语言支持OOP范式,Swift语言则可以支持FP范式,而Web前端中的JavaScript可以支持上述列出的所有编程范式。
顾名思义,函数式编程是使用函数来进行高效处理数据或数据流的一种编程方式。在数学中,函数的三要素是定义域、值域和**对应关系。假设A、B是非空数集,对于集合A中的任意一个数x,在集合B中都有唯一确定的数f(x)和它对应,那么可以将f称为从A到B的一个函数,记作:y=f(x)。在函数式编程中函数的概念和数学函数的概念类似,主要是描述形参x和返回值y之间的对应关系,**如下图所示:
温馨提示:图片来自于简明JavaScript函数式编程——入门篇[7]。
简单示例
尽管你对函数式编程的概念有所了解,但是你仍然不知道函数式编程到底有什么特点。这里我们仍然拿OOP编程范式来举例,假设希望通过OOP编程来解决数学的加减乘除问题:
classMathObject{constructor(privatevalue:number){}publicadd(num:number):MathObject{this.value+=num;returnthis;}publicmultiply(num:number):MathObject{this.value*=num;returnthis;}publicgetValue():number{returnthis.value;}}consta=newMathObject(1);a.add(1).multiply(2).add(a.multiply(2).getValue());复制代码我们希望通过上述程序来解决(1+2)*2+1*2的问题,但实际上计算出来的结果是24,因为在代码内部有一个this.value的状态值需要跟踪,这会使得结果不符合预期。接下来我们采用函数式编程的方式:
functionadd(a:number,b:number):number{returna+b;}functionmultiply(a:number,b:number):number{returna*b;}consta:number=1;constb:number=2;add(multiply(add(a,b),b),multiply(a,b));复制代码以上程序计算的结果是8,完全符合预期。我们知道了add和multiply两个函数的实际对应关系,通过将对应关系进行有效的组合和传递,达到了最终的计算结果。除此之外,这两个函数还可以根据数学定律得出更优雅的组合方式:
add(multiply(add(a,b),b),multiply(a,b));//根据数学定律分配律:a*b+a*c=a*(b+c),得出://(a+b)*b+a*b=(2a+b)*b//简化上述函数的组合方式multiply(add(add(a,a),b),b);复制代码我们完全不需要追踪类似于OOP编程范式中可能存在的内部状态数据,事实上对于数学定律中的结合律、交换律、同一律以及分配律,上述的函数式编程代码足可以胜任。
原则
通过上述简单的例子可以发现,要实现高可复用的函数**(对应关系)**,一定要遵循某些特定的原则,否则在使用的时候可能无法进行高效的传递和组合,例如
如果你之前经常进行无原则性的代码设计,那么在设计过程中可能会出现各种出乎意料的问题(这是为什么新手老是出现一些稀奇古怪问题的主要原因)。函数式编程可以有效的通过一些原则性的约束使你设计出更加健壮和优雅的代码,并且在不断的实践过程中进行经验式叠加,从而提高开发效率。
特点
一等公民
在JavaScript中,函数的使用非常灵活,例如可以对函数进行以下操作:
纯函数
纯函数是是指在相同的参数调用下,函数的返回值唯一不变。这跟数学中函数的映射关系类似,同样的x不可能映射多个不同的y。使用函数式编程会使得函数的调用非常稳定,从而降低Bug产生的机率。当然要实现纯函数的这种特性,需要函数不能包含以下一些副作用:
从以上常见的一些副作用可以看出,纯函数的实现需要遵循最小意外原则,为了确保函数的稳定唯一的输入和输出,尽量应该避免与函数外部的环境进行任何交互行为,从而防止外部环境对函数内部产生无法预料的影响。纯函数的实现应该自给自足,举几个例子:
可缓存性和可测试性基于纯函数输入输出唯一不变的特性,可移植性则主要基于纯函数不依赖外部环境的特性。这里举一个可缓存的例子:
interfaceICache
在函数式编程的简单示例中已经可以清晰的感受到函数式编程绝对不能依赖内部状态,而在纯函数中则说明了函数式编程不能依赖外部的环境或状态,因为一旦依赖的状态变化,不能保证函数根据对应关系所计算的返回值因为状态的变化仍然保持不变。
这里单独讲解一下数据不可变,在JavaScript中有很多数组操作的方法,举个例子:
constarr=[1,2,3];console.log(arr.slice(0,2));//[1,2]console.log(arr);//[1,2,3]console.log(arr.slice(0,2));//[1,2]console.log(arr);//[1,2,3]console.log(arr.splice(0,1));//[1]console.log(arr);//[2,3]console.log(arr.splice(0,1));//[2]console.log(arr);//[3]复制代码这里的slice方法多次调用都不会改变原有数组,且会产生相同的输出。而splice每次调用都在修改原数组,且产生的输出也不相同。在函数式编程中,这种会改变原有数据的函数已经不再是纯函数,应该尽量避免使用。
阅读链接:如果想要了解更深入的函数式编程知识点,可以额外阅读函数式编程指北[8]。
ThedirectionCSSpropertysetsthedirectionoftext,tablecolumns,andhorizontaloverflow.Usertlforlanguageswrittenfromrighttoleft(likeHebreworArabic),andltrforthosewrittenfromlefttoright(likeEnglishandmostotherlanguages).
具体查看:developer.mozilla.org/en-US/docs/…[9]
Theconsoleobjectprovidesaccesstothebrowser'sdebuggingconsole(e.g.theWebconsole[10]inFirefox).Thespecificsofhowitworksvariesfrombrowsertobrowser,butthereisadefactosetoffeaturesthataretypicallyprovided.
这里列出一些我常用的API:
具体查看:developer.mozilla.org/en-US/docs/…[11]
在JavaScript中利用事件循环机制[12](EventLoop)可以在单线程中实现非阻塞式、异步的操作。例如
我们重点来看一下常用的几种编程方式(Callback、Promise、Generator、Async)在语法糖上带来的优劣对比。
Callback
Callback(回调函数)是在Web前端开发中经常会使用的编程方式。这里举一个常用的定时器示例:
exportinterfaceIObj{value:string;deferExec():void;deferExecAnonymous():void;console():void;}exportconstobj:IObj={value:'hello',deferExecBind(){//使用箭头函数可达到一样的效果setTimeout(this.console.bind(this),1000);},deferExec(){setTimeout(this.console,1000);},console(){console.log(this.value);},};obj.deferExecBind();//helloobj.deferExec();//undefined复制代码回调函数经常会因为调用环境的变化而导致this的指向性变化。除此之外,使用回调函数来处理多个继发的异步任务时容易导致回调地狱(CallbackHell):
fs.readFile(fileA,'utf-8',function(err,data){fs.readFile(fileB,'utf-8',function(err,data){fs.readFile(fileC,'utf-8',function(err,data){fs.readFile(fileD,'utf-8',function(err,data){//假设在业务中fileD的读写依次依赖fileA、fileB和fileC//或者经常也可以在业务中看到多个HTTP请求的操作有前后依赖(继发HTTP请求)//这些异步任务之间纵向嵌套强耦合,无法进行横向复用//如果某个异步发生变化,那它的所有上层或下层回调可能都需要跟着变化(比如fileA和fileB的依赖关系倒置)//因此称这种现象为回调地狱//....});});});});复制代码回调函数不能通过return返回数据,比如我们希望调用带有回调参数的函数并返回异步执行的结果时,只能通过再次回调的方式进行参数传递:
//希望延迟3s后执行并拿到结果functiongetAsyncResult(result:number){setTimeout(()=>{returnresult*3;},1000);}//尽管这是常规的编程思维方式constresult=getAsyncResult(3000);//但是打印undefinedconsole.log('result:',result);functiongetAsyncResultWithCb(result:number,cb:(result:number)=>void){setTimeout(()=>{cb(result*3);},1000);}//通过回调的形式获取结果getAsyncResultWithCb(3000,(result)=>{console.log('result:',result);//9000});复制代码对于JavaScript中标准的异步API可能无法通过在外部进行try...catch...的方式进行错误捕获:
try{setTimeout(()=>{//下述是异常代码//你可以在回调函数的内部进行try...catch...console.log(a.b.c)},1000)}catch(err){//这里不会执行//进程会被终止console.error(err)}复制代码上述示例讲述的都是JavaScript中标准的异步API,如果使用一些三方的异步API并且提供了回调能力时,这些API可能是非受信的,在真正使用的时候会因为执行反转(回调函数的执行权在三方库中)导致以下一些问题:
举个简单的例子:
interfaceILib
Callback的异步操作形式除了会造成回调地狱,还会造成难以测试的问题。ES6中的Promise(基于PromiseA+[21]规范的异步编程解决方案)利用有限状态机[22]的原理来解决异步的处理问题,Promise对象提供了统一的异步编程API,它的特点如下:
温馨提示:有限状态机提供了一种优雅的解决方式,异步的处理本身可以通过异步状态的变化来触发相应的操作,这会比回调函数在逻辑上的处理更加合理,也可以降低代码的复杂度。
Promise对象的执行状态不可变示例如下:
constpromise=newPromise
//回调地狱constdoubble=(result:number,callback:(finallResult:number)=>void)=>{//Mock第一个异步请求setTimeout(()=>{//Mock第二个异步请求(假设第二个请求的参数依赖第一个请求的返回结果)setTimeout(()=>{callback(result*2);},2000);},1000);};doubble(1000,(result)=>{console.log('result:',result);});复制代码温馨提示:继发请求的依赖关系非常常见,例如人员基本信息管理系统的开发中,经常需要先展示组织树结构,并默认加载第一个组织下的人员列表信息。
如果采用Promise的处理方式则可以规避上述常见的回调地狱问题:
constfirstPromise=(result:number):Promise
constfirstPromise=(result:number):Promise
constfirstPromise=(result:number):Promise
constpromise=newPromise
Promise相对于Callback对于异步的处理更加优雅,并且能力也更加强大,但是也存在一些自身的缺点:
温馨提示:手写Promise是面试官非常喜欢的一道笔试题,本质是希望面试者能够通过底层的设计正确了解Promise的使用方式,如果你对Promise的设计原理不熟悉,可以深入了解一下或者手动设计一个。
Generator
Promise解决了Callback的回调地狱问题,但也造成了代码冗余,如果一些异步任务不支持Promise语法,就需要进行一层Promise封装。Generator将JavaScript的异步编程带入了一个全新的阶段,它使得异步代码的设计和执行看起来和同步代码一致。Generator使用的简单示例如下:
constfirstPromise=(result:number):Promise
next可以不停的改变状态使得yield得以继续执行的代码可以变得非常有规律,例如从上述的手动执行Generator函数可以看出,完全可以将其封装成一个自动执行的执行器,具体如下所示:
需要注意的是Generator函数的返回值是一个Iterator遍历器对象,具体如下所示:
constfirstPromise=(result:number):Promise
constfirstPromise=(result:number):Promise
Async
Async是Generator函数的语法糖,相对于Generator而言Async的特性如下:
举个简单的示例:
constfirstPromise=(result:number):Promise
上述代码是阻塞式执行,nextPromise需要等待firstPromise执行完成后才能继续执行,如果希望两者能够并发执行,则可以进行下述设计:
constfirstPromise=(result:number):Promise
constfirstPromise=(result:number):Promise
constfirstPromise=(result:number):Promise
业务思考更多的是结合基础知识的广度和深度进行的具体业务实践,主要包含以下几个方面:
笔试更多的是考验应聘者的逻辑思维能力和代码书写风格,主要包含以下几个方面: