JS数据类型,类型转换,显式和隐式强制类型转换

1类型

定义:对开发者来说,类型是值的内部特征,它定义了值的行为,以使其区别于其他值。

理解类型,掌握类型,有助于理解掌握类型转换

1.1内置类型

七种类型:

  1. 空值(null)
  2. 未定义(undefined)
  3. 布尔值(boolean)
  4. 数字(number)
  5. 字符串(string)
  6. 对象(object)
  7. 符合(symbol)

除对象外,其他统称为基本类型

除了null,其他类型用typeof检查都返回类型名字符串,null返回的是'object'。js是动态语言,变量可以随时持有任意类型的值。

2值

2.1数组

数组可以容纳任何类型的值,类数组可以转为数组,Array.from(...);

2.2字符串

字符串可以使用一些数组的方法,如length,下标取值等。字符串是不可变的,指的是字符串的成员函数不会改变其原始值,而数组的成员函数是在其原始值上操作

a = 'qq'
a.toUpperCase() // QQ
a // 'qq'
b = [1,1]
b.push(3)
b // [1,1,3]

许多数组函数处理字符串很方便,虽然字符串没有,但是可以借用。

a = 'qwe'
let c = Array.prototype.join.call(a, '-')
let d = Array.prototype.map.call(a, function(v) {
    return v.toUpperCase() + '.'
}).join('')

c // q-w-e
d // Q.W.E

但是数组的reverse()函数是可变更成员的,字符串无法借用,因为字符串不可变。可以先把字符串split转数组再使用reverse再join转字符串

2.3数字

js只有一种数值类型,number,包括整数和带小数的十进制数。js的整数就是没有小数的十进制数,所以42.0等同于42.

js的数字类型是基于IEEE754标准实现,也被称为“浮点数”,js使用的是“双精度”格式,即64位二进制。

二进制浮点数最大问题是会出现如下问题:

0.1 + 0.2 === 0.3 // false

从数学角度应该是true的,但是二进制浮点数中的0.1+0.2并不是十分精确,而是一个接近0.30000000000000004的数字

2.3.3 整数安全范围

能够安全呈现的最大整数是9007199254740991,最小是-9007199254740991。可以用Number.isInteger()检测是否位为整数。注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isI nteger可能会误判。 Number.isInteger(3.0000000000000002) // true 上面代码中,Number.isInteger的参数明明不是整数,但是会返回true。原因就是这个小数的精度达到了小数点后16个十进制位,转成二进制位超过了53个二进制位,导致最后的那个2被丢弃了。

可以用Number.isSafeInteger()检测是否为安全整数。

2.4特殊数值

js数据类型中有几个特殊的值需要注意

2.4.1不是值的值

undefined类型只有一个值,即undefined,null类型只有一个值,即null。

undefined一般用来表示没有值,未赋值

null一般用来表示空值

其中,null是特殊关键字,不是标识符,不能用来做变量使用,undefined是一个标识符,可以被当作变量和赋值。

2.4.2特殊数字

1.不是数字的数字

如果数学运算符的操作数不是数字类型(或者无法解析为常规的十进制或十六进制数字)就无法返回一个有效数字,这种情况返回NaN,意指不是一个数字,或者理解为无效数值,坏数值更好。

如:
var a = 2 / 'qq' // NaN
typeof a === 'number' // true

Nan仍是个数字类型,更特殊的是NaN和自身不相等。可以用isNaN来判断一个值是否为NaN。但是实际上

isNaN(NaN) // true
isNaN('qq') // true

对于非数字也返回true,不过ES6有Number.isNaN()可以成功检测

Number.isNaN(NaN) // true
Number.isNaN('qq') // false

2.无穷数

a=1/0 // Infinity
2/a // 0
-2/a //-0

3.零 js有一个常规0,也叫+0,和一个-0

0/-3 // -0
0*-3 // -0
-0 == 0 // true
-0 === 0 // true

加减法不会得到-0。为什么需要-0呢,有些应用程序的数据,数字的符号位用来表示其他信息(如方向),此时如果一个值位0的变量失去了它的符合,方向信息就会丢失。

2.4.3特殊等式

通常判断两个值是否相等用== 和===。ES6新加入了一个工具方法Object.is(...),可以用来判断两个值是否绝对相等,可以用来处理上述特殊情况,但是一般判断不用,因为

==
===

效率更高

2.5值和引用

赋值和参数传递可以通过值复制和引用复制来完成,例如

var a = 2
var b = a // b是a的值的一个复本
b++
a // 2
b // 3

var c = [1, 2, 3]
var d = c // d是[,1, 2, 3]的一个引用
d.push(4)
c // [1, 2, 3, 4]
d // [1, 2, 3, 4]

简单值总是通过值复制的方法赋值/传递,包括(null,undefined,字符串,数字,布尔值,symbol)。

复合值,对象(包括数组和封装对象)和函数,总是通过引用复制的方式来赋值/传递

由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。如:

var a = [1]
var b = a
a // [1]
b // [1]

b = [2]
a // [1]
b // [2]

3.原生函数

js有内建函数,也叫原生函数,常用的有

String()
Number()
Boolean()
Array()
Object()
Function()
Date()
Error()
Symbol()

原生函数可以被当作构造函数使用,但是构造出对象三这样的:

var a = new String('qq')
typeof a // 是object,不是String
a instanceof String // true
Object.prototype.toString.call(a) // "[object String]"

通过构造函数创建出的是封装来基本类型值的封装对象,typeof返回的是对象类型的子类型

3.1内部属性

所有typeof返回是“object”的对象(如数组)都包含一个内部属性[[Class]],这个属性无法直接访问,一般通过Object.prototype.toString.call(...)访问。

3.2封装对象包装

由于基本类型值没有.length和.toString()这样的属性和方法,需要通过封装对象 才能访问,此时js会自动为基本类型值包装一个封装对象:

var a = 'abc'
a.length // 3

一般情况下不需要使用封装对象,让js自己决定,优先考虑用'abc',42这种基本类型值,而非new String()等。可以用.valueOf()获取封装对象中的基本类型值。

3.3原生函数作为构造函数

对于数组,对象,函数和正则,我们通常喜欢用字面量的形式来创建。实际上,使用字面量和构造函数效果是一样的。应该尽量避免使用构造函数,因为有时候会有意外效果。

对于数组

var a = new Array(1,2,3) // 不是必须要带new关键字,不带会自动不全
a // [1,2,3]
b = [1,2,3]
b // [1,2,3]

但是当Array构造函数只有一个参数时new Array(3) // [empty × 3],该参数会被当作数组预设长度,创建出一个空数组,有三个空单元,我们把包含至少一个空单元的数组叫稀疏数组。这种结构会导致一些怪异行为,永远不要创建和使用空单元数组。

除非万不得已,否则尽量不要使用Object(...),Function(...),RegExp(...)

3.4原生原型

原生构造函数有自己的.prototype对象,如Array.prototype,这些对象包含其对应子类型所特有的行为特征。

indexOf()
charAt()
substr()
trim()

借助原型代理,所有字符串都可以访问这些方法,

4强制类型转换

4.1值类型转换

将一个值从一种类型转换为另一种类型通常称为类型转换,这是显式的情况,隐式的被称为强制类型转换 如

var a = 22
var b = a + '' // '22',隐式强制类型转换
var c = String(a) // '22' ,显式强制类型转换

toString(),非字符串到字符串的强制类型转换,

null 转 'null'
undefined 转 'undefined'
var a = [1,2]
a.toString() // '1,2'

toNumber(),转换为数字

Number(true) // 1
Number(false) // 0
Number(undefined) // NaN
Number(null) // 0
Number('qq') // NaN
Number('22') // 22

对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字类型值,则再遵循以上规则将其强制转换为数字。为了将值转换为相应基本类型值,抽象操作ToPrimitive(见ES5规范9.1)会检查该值是否有valueOf()方法,有且返回基本类型值,使用该值进行强制类型转换,没有则使用toString()的返回值(如果有)进行强制类型转换。如果均不返回基本类型值,会TypeError

toBoolean

布尔值,true和false,虽然1和0可以通过强制类型转换为true和false,但它们不是一回事。

js中的值可以分为两大类: 1 可以被强制类型转换为false的值 2 其他(强制类型转换为true)

以下是假值false:

undefined
null
false
+0   -0  NaN
""

真值

假值之外的都是真值,ru

[]
{}
function(){}
'false'
'0'
"''"

显示强制类型转换是那些显而易见的类型转换,我们应该尽可能的将类型转换表达清楚,以免坑别人,可读性越高,越容易理解。

4.2字符串和数字之间的显示类型转换

var a = 42
var b = String(a) || a.toString()
var c = '3.14'
var d = Number(c) || +c

日期转数字
var d = new Date() || new Date().getTime() || Date.now()
+d // 1586784116068

4.2.1显示转换为布尔值

var a = "0"
var b = []
var c = {}

var d = ''
var e = 0
var f = null

Boolean(a) // true
Boolean(b) // true
Boolean(c) // true

Boolean(d) // false
Boolean(e) // false
Boolean(f) // false

虽然Boolean是显示的,但是并不常用,一元运算符!显示的将值强制类型转换为布尔值,但是它同时还将真值转换为假值,所以显示强制类型转换为布尔值最常用方法是!!,更加清晰易读

4.4隐式强制类型转换

隐蔽的强制类型转换,许多人对此诟病,认为它让代码变得晦涩难懂,会带来负面影响,但是如果能灵活运用,它是非常不错的,不能因噎废食

4.4.1字符串和数字之间的隐式强制类型转换

+运算符可以用于数字运算加法,也能用于字符串拼接。如

var a = '22'
var b = '0'

var c = 33
var d = 0

a + b // '220'
c + d // 33

a + c // '2233'

var x = [1,2]
var y = [3,4]
x + y // '1,23,4'

简单来说,如果+其中一个操作数是字符串,(或者通过之前的规则得到字符串)则执行字符串拼接,否则执行数字加法。

我们可以使用空字符串""和数字相+得到字符串

var a = 22
a + '' // '42'

a + ''和Srring(a)有一个细微差别要注意:根据ToPrimitive()抽象操作规则,a+''会对a调用valueOf()方法,然后通过ToString抽象操作将返回值转换为字符串,而String(a)是直接调用ToString(),一般不会遇到这种问题,除非是对象,如:

var a = {
    valueOf: function() { return 22 },
    toString: function() { return 2 }
}
a + '' // '22'
String(a) // '2'

再看看从字符串强制类型转换为数字

var a = '3.14'
var b = a - 0
b // 3.14

-是数字减法运算符,a - 0会将a强制类型转换为数字,也可以使用* 和 /,因为这两个运算符也只适用于数字,不过不常见。

对象的-和+类似,如

var a = [11]
var b = [2]
a - b // 9

a和b先被转为字符串(通过toString)再转换为数字

4.4.2隐式强制类型转换为布尔值

以下情况会发生布尔值隐式强制类型转换

  1. if(...)语句中的条件判断表达式
  2. for(...;...;...)语句中的条件判断表达式的第二个
  3. while(...), doWhile(...)循环中的条件判断表达式
  4. 三元运算符中的条件判断表达式
  5. 逻辑运算符||和&&左边的操作数(作为条件判断表达式)

非布尔值会被隐式强制类型转换为布尔值,如

var a = 42
var b = 'qq'
var c
var d = null

if (a) { console.log('YES') } // 'YES'
while(c) { console.log('no') }
c = d ? a : b // 'qq'
if((a && d) || c) { console.log('yes') } // 'yes'

4.4.3|| 和&&

|| 和 && 返回的不一定是布尔值,而是两个操作数中的其中一个的值 如

var a = 22
var b = 'abc'
var c = null

a || b // 22
a && b // 'abc'
c || b // 'abc'
c && b // null

||和&&会对第一个操作数判断,,如果不是布尔值则先进行ToBoolean强制类型转换,然后再判断。

对于|| 如果第一个判断为true,则返回第一个值否则第二个值。

&&则相反,第一个判断为true返回第二个值,false返回第一个值。

4.5相等比较

== 和 ===

==允许在相等比较中进行强制类型转换,而‘===不允许。

如果比较的两个值类型相同,==和===使用相同算法,
如果类型不同,就要考虑有没有强制类型转换的必要,
有就用==,没有就用===.

另外要注意NaN不等于NaN,+0不等于-0。!= 和 !== 的判断方式和==,===一样。

字符串和数字之间相等比较

1.如果type(x)是数字,type(y)是字符串,则返回x == toNumber(y)结果

1.如果type(x)是字符串,type(y)是数字,则返回toNumber(x) == y结果

对象和非对象的相等比较

对象和基本类型之间相等比较:

1,如果type(x)是字符串或数字,type(y)是对象,返回x == ToPrimitive(y)的结果

2.如果type(x)是对象,type(y)是数字或字符串,返回ToPrimitive(x) == y的结果

4.5.1其他类型值和布尔类型值的相等比较

var a = '42'
var b = true
a == b // false

a是个真值,为什么==结果不是true,规范说:

1.如果type(x)是布尔类型,则返回toNumber(x) == y结果

2.如果Type(y)是布尔类型,则返回x == toNumber(y)结果==

type(b)是布尔值,所以b被转换为数字1,'42' 1再变42==1,结果false

4.5.2null和undefined

对于null和undefined,==比较也有隐式强制类型转换,null == undefined // true,它们也与自身相等。

4.5.3<

a < b也涉及隐式类型转换,分为2种情况,1,比较双方都是字符串,和其他情况。

比较双方先进行toPrimitive,如果结果出现非字符串,就根据toNumber规则将双方强制类型转换为数字来比较,如

var a = [42]
var b = ['44']
a < b // true
b < a // false

5语法

相比词法,语法有点陌生,很多时候二者是一个意思,都是语言规则的定义,虽然有时有细微差别,js语法定义了词法规则是如何构成可运行的程序代码的。

5.1语句和表达式

我们常会把语句和表达式混为一谈,但是二者是有重要区别的。对于英语来说,句子是完整表达某个意思的一组词,有一个或多个短语组成,由标点连接,短语可以由更小短语组成。这是英语语法。

对于js,语句相当于句子,表达式相当于短语,运算符相当于标点和连接词。

js中表达式可以返回一个结果值,如:

var a = 3 * 6
var b = a
b

这里3 * 6是一个表达式(结果为18),第二行a也是,第三行b也是,结果都是18.

这三行代码都是包含表达式的语句,var a = 3 * 6和var b = a称为“声明语句”,因为它们声明了变量并赋值,a = 3 * 6,b = a(不带var)叫“赋值表达式” 第三行只有一个表达式b,同时它也是个语句,这样的通常叫:表达式语句“

5.1.1语句的结果值

语句都有一个结果值,可以在浏览器控制台输入语句,如输入var a = 18,回车后会显示undefined。ES5规范规定变量声明算法实际有个返回值,但是被变量语句算法屏蔽了(for in除外),最后返回undefined

有个常见的坑:

1.[] + {} // "[object object]"
2.{} + [] // 0

在1中:根据规范,如果某个操作数是字符串或者能通过以下步骤转换为字符串,+将进行拼接操作。如果一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作(先对操作数进行调用valueOf方法返回基本类型值,然后通过调用ToString抽象操作转换为字符串,valueOf无法返回基本类型值则直接调用ToString转字符串),该操作再调用[[DefaultValue]],以数字作上下文。所以[]调用valueOf返回是[],调用toString返回'',{}调用valueOf返回{},调用toString返回"[object object]",最终返回"[object object]"

在2中,{}被当作独立空代码块,代码块结尾不需要分号,所以不存在语法错误,最后 + []将[]显式强制转换为数字,0

5.2运算优先级

|| 和 && 返回其中一个操作数的值,当只有两个操作数时逻辑很好理解,那么如果多个呢? 就要运用“运算符优先级”规则

用,来连接一系列语句时,它的优先级最低,&&的优先级高于=,&&优先级高于||。

我们常使用

if (obj && obj.name) {...}

这种“短路”操作,防止obj未赋值导致obj.name出错。根据运算优先级,对于复杂的多个运算符判断,适当使用()将其包裹,使逻辑清晰,维护方便

5.3try ...finally

finally代码总会在try之后执行,若有catch在catch后执行,无论什么情况,finally最后一定会被调用。

== 要注意的==是,如果try中有return返回一个值,如

function fn() {
    try {
        return 22
    } finally {
        console.log('hello')
    }
    console.log('end')
}

console.log(fn())
// hello
//22

throw也是一样。如果finally中有抛出异常,则函数会在此终止,如果try中有return 返回值,则该值会被丢弃。如果finally中有return,则会覆盖try和catch中的返回值。

6异步和性能

一、为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

二、任务队列 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步

三、事件和回调函数 "任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

四、Event Loop 主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

setTimeout(_ => console.log(4))

new Promise(resolve => { resolve() console.log(1) }).then(_ => { console.log(3) })

console.log(2)

setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。 所有会进入的异步都是指的事件回调中的那部分代码 也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。 在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。 所以就得到了上述的输出结论1、2、3、4。

7。尾调用优化

一、什么是尾调用?

尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

function f(x){
  return g(x);
}

上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。

以下两种情况,都不属于尾调用。

// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

上面代码中,情况一是调用函数g之后,还有别的操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。

尾调用不一定出现在函数尾部,只要是最后一步操作即可。

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。

二、尾调用优化

尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g(3) 的调用记录。

这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。

三、尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120
上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。


function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。

function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120
上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 。

第二种方法就简单多了,就是采用ES6的函数默认值。


function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。

总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持"尾调用优化"的语言(比如Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值