内置属性
JavaScript中函数是一种特殊的可以执行的对象
所以在函数自身上也可以有自己特有的属性和方法,如call,bind,apply,prototype等
函数也有对应的name和length属性
| 属性 | 说明 |
|---|---|
| name | 存储了函数的名称 |
| length | 记录着函数除剩余参数和有默认值参数之外的形参的个数 |
function fun() {}
const bar = function() {}
console.log(fun.name) // => fun
console.log(bar.name) // => bar
function fun(name, age = 23, ...args) {}
fun('Klaus', 25, 1.73, 'shanghai')
// 函数的length计算的是形参的个数,不是实参的个数
// 对于剩余参数和有默认值的参数,这两类参数不计算在函数的length中
console.log(fun.length) // => 1
arguments
arguments是只存在于非箭头函数内部的用于接收所有实参的可迭代类数组对象
-
arguments用于接收函数所有传入的实参
-
arguments是一个可迭代对象,可以使用for-of遍历
-
箭头函数内部没有arguments对象
-
arguments是一个类数组对象(array-like object)
-
arguments拥有数组的一些特性,比如说length,比如可以通过index索引来访问
-
arguments没有数组的一些方法,比如filter、map等
-
所以在实际开发过程中,我们经常需要将arguments转换为数组对象
- 遍历arguments,添加到一个新数组中
- 调用数组slice函数的call方法或apply方法
- 使用Array.from方法
- 使用展开运算符
function foo() {
// slice方法是一个可以对数组进行截取的纯函数方法
// 如果省略slice方法的参数,其功能就等价于浅拷贝,即对整个数组进行截取
// 因为slice内部是对数组进行迭代后放入一个新数组中并返回
// 而arguments又是一个可迭代对象
// 所以slice方法本质是在arguments上使用的
// 但是slice是数组方法,而arguments是伪数组对象
// 因为我们需要使用apply方法或call方法来改变slice方法在调用的时候其内部this的指向
// 为了调用slice方法,我们需要使用[].slice()或Array.prototype.slice()来对slice方法进行调用
const arr = [].slice.apply(arguments)
console.log(arr)
}
foo(1, 2, 3, 4, 5)
function foo() {
// Array.from方法可以将一个可迭代对象转换为原生数组对象
// 所以Array.from对象的参数必须是一个可迭代对象
console.log(Array.from(arguments))
// 展开运算符的本质是对参数进行遍历,并将遍历出来的值依次放到一个新容器中(在这里就是数组)
// 所以其也可以将一个可迭代对象转换为对应的数组
console.log([...arguments])
}
foo(1, 2, 3, 4, 5)
rest参数
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中
剩余参数是以... 为前缀的,剩余参数的类型为原生数组,它会接收所有的没有对应形参接收的实参
剩余参数必须放到最后一个位置,否则会报错
因为如果剩余参数不位于参数列表中的最后一个参数的话,剩余参数后边的参数将永远无法获取到对应的实参值
剩余参数和arguments的区别
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
- arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作
所以剩余参数的产生是用来取代早期的arguments参数的,在开发中更推荐使用剩余参数,而不是arguments参数
纯函数
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
当一个函数满足如下条件的时候,该函数就是一个纯函数:
- 确定的输入,在任何情况下都一定会产生确定的输出
- 函数只能和输入或输出值有关,不可以依赖外部状态,例如外部的自由变量,或者I/O设备产生的外部输入输出
- 在函数在执行过程中,不能产生任何形式的副作用
- 所谓副作用就是在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响
- 比如修改了全局变量,修改参数或者改变外部的存储
在实际开发中,我们所使用的函数和方法可能是第三方库编写的,这也就意味着我们是无法知道函数的内部实现
因此如果该函数是一个非纯函数的时候,就可能会对函数外的状态或传入的参数产生预期之外的副作用,
而这往往是产生bug的 “温床”
在实际开发中,并不是所有的函数都必须是纯函数,例如splice方法就不是一个纯函数方法
但是在某些环境下,我们需要保证我们所编写的函数是纯函数,以避免出现bug或便于后期的维护
例如在React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改
柯里化
柯里化(currying)是属于函数式编程里面一个非常重要的概念,是一种关于函数的高阶技术
柯里化是把接收多个参数的函数,变成接受部分参数的函数,该转换后的函数会返回接受余下的参数,而且返回一个新的函数
所以柯里化是一个函数的转换过程,转换后所得到的函数被称之为柯里化函数
例如函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)
function sum(num1, num2, num3) {
return num1 + num2 + num3
}
// 将sum函数进行柯里化转换
const sum1 = num1 => num2 => num2 => num1 + num2 + num3
柯里化的优点
职责单一
对于柯里化的每一层函数,他只接收单一的参数,也就是对单一的参数所对应的处理逻辑进行编写
即柯里化将在一个函数中进行处理的逻辑拆分到了多个函数中进行实现,每层函数只要实现自己内部职责即可
const curryFun = num1 => {
// 在第一层函数中,只需要对num1所对应的逻辑进行处理
num1 = num1 * 2
// 第二层函数中,只需要对象num2所对应的逻辑进行处理
// 并不需要在处理num1所对应的逻辑,因为num1所对应的逻辑在第一层的时候已经完成
return num2 => {
num2 = num2 ** 2
// 所有参数逻辑全部处理完毕后的逻辑
// 可以单独在编写一层函数进行处理,也可以直接在最后一层中直接实现
console.log(num1 + num2)
}
}
参数复用
// 在本案例中,日志的第一个类型往往是固定的,所以我们可以将日志打印函数转换为柯里化函数
// 从而对第一个参数进行复用
const log = type => msg => console.log(`${type} : ${msg}`)
const fixLog = log('fix')
fixLog('修复功能1')
fixLog('修复功能2')
fixLog('修复功能3')
const featureLog = log('feature')
featureLog('新功能1')
featureLog('新功能2')
featureLog('新功能3')
柯里化的缺点
柯里化函数本质上是使用了闭包,所以频繁的创建柯里化函数必然会增加堆内存消耗的增加
此外柯里化函数将一个函数调用转换成了多次的函数调用,因此也会导致栈内存消耗的增加
因此虽然柯里化函数在某种程度上可以简化函数的调用,但是不应该频繁使用柯里化函数
而是只在必要的情况下,再去使用函数的柯里化
自动柯里化函数
function autoCurryFun(fn) {
let params = []
return function fun(...args) {
if (args.length) {
params = [...params, ...args]
return fun
} else {
return fn.call(this, ...params)
}
}
}
// test code
const sum = (num1, num2, num3) => console.log(num1 + num2 + num3)
const currySum = autoCurryFun(sum)
// 以下都是柯里化的调用方式
currySum(10)(20)(30)()
currySum(10, 20)(30)()
currySum(10, 20, 30)()
组合函数
组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式,方法
- 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的
- 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复
- 此时我们可以对重复调用的行为进行封装,这个新封装出来的函数就被称之为组合函数(Compose Function)
function sum(num1, num2) {
return num1 + num2
}
function doubleNum(num) {
return num ** 2
}
// 每次这么调 必然存在重复代码
console.log(doubleNum(sum(2, 3)))
console.log(doubleNum(sum(22, 3)))
console.log(doubleNum(sum(2, 23)))
// 构建组合函数
function genComposeFun(fn1, fn2) {
return function(...args) {
return fn2(fn1(...args))
}
}
const composeFun = genComposeFun(sum, doubleNum)
console.log(composeFun(1, 3))
自动组合函数
function autoComposeFn(...fns) {
// edge case
if (!fns.length) throw new TypeError('parameters cannot be empty')
for (const fn of fns) {
if (typeof fn !== 'function') {
throw new TypeError('the parameters need to be the function')
}
}
// 返回组合函数
return function(...args) {
// 用于接收每一次函数执行的返回值
let res
for (let index in fns) {
// for-in获取的索引类型是string类型的
if (index === '0') {
// 第一个函数接收组合函数传入的参数并进行调用
res = fns[0].apply(this, args)
} else {
// 之后每次函数调用 都使用前一个函数的返回值作为函数参数并传入
res = fns[index].call(this, res)
}
}
return res
}
}
// Test Code
function sum(num1, num2) {
return num1 + num2
}
function doubleNum(num) {
return num ** 2
}
const compseFn = autoComposeFn(sum, doubleNum, console.log)
compseFn(2, 3)
其它补充
with
with语句 扩展一个语句的作用域链
不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源
const info = {
name: 'Klaus'
}
// with语句开启了一个块级作用域,且该块级作用域对应的VO是info对象
with(info) {
console.log(name) // => Klaus
}
eval
内建函数 eval 允许执行一个代码字符串
- eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行
- eval会将最后一句执行语句的结果,作为返回值
不推荐在开发中使用eval函数
- eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则)
- eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险
- eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化,即无法被TurboFan收集并优化
let msg = 'Hello World'
// 在eval内部执行的代码,作用域和不使用eval执行时候其作用域规则是一致的
// 所以可以在eval执行字符串中使用全局变量msg
// eval字符串中定义的username 也是全局变量
eval("var username = 'Klaus'; let age = 23; console.log(msg);")
console.log(username) // => Klaus
// eval执行字符串中 使用let/const中定义的变量不可以在eval外使用
console.log(age) // error
严格模式
长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题
新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码
但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中
所以在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode)
- 严格模式是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“
- 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行
- 一些应该被淘汰但是为了兼容旧版本没有被移除的写法在严格模式下将不再被适用
严格模式对正常的JavaScript语义进行了一些限制:
- 严格模式通过 抛出错误 来消除一些原有的 静默(silent)错误
- 严格模式使JS的运行效率更高(不需要对一些特殊的语法进行处理)
- 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法(虽然部分浏览器支持了新功能,但是并不是所有浏览器都支持了新特性,存在兼容性问题)
严格模式通过在文件或者函数开头使用 use strict 来开启
没有类似于 "no use strict" 这样的指令可以使程序返回默认模式, 严格模式一旦开启,将无法关闭
现代 JavaScript 支持 “class” 和 “module” ,它们会自动启用 use strict
对于使用打包工具,打包后形成的JS文件,都会默认开启严格模式
严格模式支持粒度化的迁移:
- 可以支持在js文件中开启严格模式
- 也支持对某一个函数开启严格模式
严格模式下的部分限制规则
- 无法意外的创建全局变量 - 隐式全局变量不被允许使用
- 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的操作抛出异常
- 如修改只读属性的值
- 或删除不可以被删除的属性
- 严格模式不允许函数参数有相同的名称
- 不允许0的八进制语法 -- 早期的八进制是使用0作为前缀的,现在JS是使用0b作为前缀的
- 在严格模式下,不允许使用with
- 在严格模式下,eval中定义的变量将无法在eval外进行使用
- 严格模式下,this绑定不会默认转成对象, 且不绑定全局this,即全局this的值为undefined
"use strict"
eval("var username = 'Klaus'; console.log(msg);")
// 严格模式下,eval中定义的变量 在eval外将无法获取
console.log(username) // error
"use strict"
function foo() {
console.log(this)
}
foo() // => undefined
foo.call(123) // => 123
// 在非严格模式下,null和undefined将不可以作为this的值,因此将采用默认绑定规则
// 但是在严格模式下,因为this默认不再被转换为对应包装类对象,所以可以将this的值绑定为undefined或null
foo.call(undefined) // => undefined
foo.call(null) // => null