引用类型中变量只是对真实对象的一个指针
引用类型可以无限制扩展属性
值类型: 存储在 栈(stack) 中,占据空间小,大小固定。
引用类型: 同时存储在 栈 和 堆(heap) 中,占据空间大,大小不固定。引用类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获取实体。
typeof可以精准识别出值类型,但是无法精准识别出 引用类型。
对于引用类型,typeof 只能识别出是 object 或 function。
array、null、object 都会被 typeof 识别为 object 类型
- undefined
- string
- number
- boolean
- object
- function
- symbol(ES6新增)
- BigInt(ES10新增)
以下只针对 值类型 变量计算。
加号 + :
- number + number —> number + number
- number + string —> String(number) + string
双等号 == :
- number == string —> string(number) == string
- number == '' —> boolean(number) == boolean('')
- null == undefined —> boolean(null) == boolean(undefined)
使用双等 == ,JS 会先尝试将两侧对比转换成 string 类型,看是否相等,若不相等再尝试转换成 boolean 类型,如果依然还不相等 才会返回计算结果 false
三等 === 不会出现以上情况。
除了 if(obj.a == null){...} 之外,其他地方都推荐使用 三等。
obj.a == null 相当于:if(obj.a === null || obj.a === undefined){...}
条件判断 if 语句:
会尝试将 参数 转化为 boolean 类型。
逻辑运算:
&&:对比对象均会尝试转化为 boolean 类型
|| :先将对比的前者转化为 boolean 类型,若为 true 则返回前者本身的值,若为 false 则返回 后者本身的值
! :将对比对象转化为 boolean 类型,并返回该结果的相反结果
!! :会将对比对象转化为 boolean 类型,并返回该结果
- String
- Number
- Boolean
- Array
- Object
- Function
- Date
- RegExp (regular expression)
- Error
- Symbol
Math、JSON 等等都是 JS 中是内置对象,而不是内置构造函数类型,他们的构造函数类型都是:Object
eval()、encodeURL()、isNaN() 等等都是 JS 中的内置函数,而不是内置构造函数类型,他们的构造函数类型都是:Function
NaN、Infinity、undefined、globalThis 这些都是 JS 中的内置属性值,他们是具体的值,不是函数,若使用 typeof xxx,得到的是他们值的类型(注意不是构造函数类型)
undefined:在作用域中已声明,但未赋值的变量
undeclared:还没有在作用域中声明的变量
引用 undeclared 变量会报错:ReferenceError: xx is not defined.
null:代表空对象(事实上并不是真正的对象)
在最初的 32 为系统中,为了性能考虑使用低位存储变量的类型信息,000 开头代表对象,然而 null 表示为全 0,所以错误得判断 null 也为 object,虽然目前 JS 内部已经做了修正,但这个 bug 一直沿用至今。
运行 | {} | [] |
---|---|---|
valueOf() | {} | "[object Object]" |
toString() | [] | "" |
赋值运算是从右往左进行计算执行的。
JS 在非严格模式下,若一个变量没有使用 var/let 声明,则 JS 默认会自动帮你声明。
两种情况叠加,于是,会出现下面的情况:
- let a = b = 3 其实相当于:b = 3; let a = b;
- b 会被 JS 自动定义,且为全局变量
function myFun() {
let a = b = 3
}
myFun()
console.log(b) // 3
上述代码仅在 JS 非严格模式下 可以正常运行
一道经典面试题
如何判断一个对象是数组类型?
首先 typeof xxx 是不可以的,因为 typeof 返回的是该对象的 内置类型,typeof array 得到的是 Object。
所谓 “数组类型” 是指 构造函数为 Array,一个对象是数组类型真正想表达的是:一个对象的构造函数是Array。
因此需要判断和获取 对象的构造函数 是什么。
第1种方式:
使用 instanceof 来判断 对象的原型链是否为 JS 内置构造函数 Array
const arr = []
console.log(arr instanceof Array) //true
instanceof 的本质就是不断查找(一层一层查找) 变量的
__proto__.constructor
值,直到最深处为 null 为止,若中间发现有和 要比较的类型函数相同,则返回 true
第2种方式:
使用 __proto__
来判断对象的隐式原型构造函数是否为 Aarray的构造函数
const arr = []
console.log(arr.__proto__ === Array.prototype)
或者这样写:
const arr = []
console.log(arr.__proto__.constructor === Array)
第3种方式:
const arr = []
console.log(arr.constructor === Array)
或者是:
console.log(arr.constructor.name === 'Array')
第4种方式:
const arr = []
console.log(Object.getPrototypeOf(arr) === Array.prototype)
第5种方式:
const arr = []
console.log(Array.isArray(arr))
函数 VS 构造函数
所谓函数,就是可以重复执行的代码块,只定义一次但是可以被执行或调用任意次。
从功能上来划分,函数分为:普通函数和构造函数
普通函数就是使用 function 定义并且有明确返回值且返回值的函数,主要作用是用来执行或计算。
构造函数就是使用 function 定义并且没有明确返回值的函数,JS 会给他们自动隐形添加 return this,主要作用是用来初始化一个对象。构造函数需要通过 new 来配合使用。
function myFun(){
this.xx = xx
// return this 默认普通函数会自动添加这一句
}
函数名一般采用驼峰命名法,通常约定,如果是普通函数则首字母小写,如果是构造函数则首字母大写
普通函数也可以作为构造函数存在。
箭头函数是没有构造函数的,也就是说箭头函数本身并不会默认自动添加上 return this 这行隐形的代码。
自己手工添加的 return xxx 本质上是返回函数运行结果。
函数参数
普通函数和构造函数都有 arguments 对象,该属性为一个类似数组的对象,数组元素为所有参数。
箭头函数没有 arguments 对象。
在 JS 中使用构造函数来创建新的对象,每个构造函数内部都有一个 prototype 属性,该属性是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法,而这个对象被称为这个实例的原型。
所有的引用类型(数字、对象、函数)都遵循以下 4 个原则:
-
都具有对象特性,都可以自由无限扩展属性,除了 null 以外。
-
都有一个
__proto__
属性,属性值为对象的隐式构造函数的原型 -
当试图得到一个对象的某属性时,如果对象本身(显示原型)没有这个属性,则会去他的
__proto__
(隐式原型) 中寻找有一种特殊情况,就是在 for(item in xxx){ ... } 中,只会调用该对象本身的属性,不会去尝试调用 对象隐式原型上的属性
判断一个对象属性是否为显示原型上,还是隐式原型上,使用 xxx.hasOwnProperty(xxx) 来判断
所有的函数(箭头函数除外)都有一个 prototype 属性(显式原型),属性值为该对象的显示构造函数的原型
忽略上面一段文字,重新用另外一段文字来描述原型规则:
-
所有通过 function 创建的对象,都叫函数对象
-
所有函数对象都有一个原型对象 prototype
-
所有对象上都有一个隐式原型对象
__proto__
,指向创建该对象的构造函数的原型对象 -
所有的原型对象上都有一个 constructor 对象,指向该原型对象所在的构造函数本身
JS 为了避免死循环,因此 Object.prototype 的值为 null
对象显示原型对象是可以更改的!所谓继承就是 动态 更改指定构造函数的显式原型。
function Animal(){
this.eat = function (){
console.log('eat...')
}
}
function Dog(){
this.run = function () {
console.log('run...')
}
}
//将 Dog 的显性原型更改为一个 Animal 对象实例,从而让 Dog 也具有 Animal 的属性
Dog.prototype = new Animal()
const mydog = new Dog()
mydog.eat()
mydog.run()
还有另外一种继承方式,即直接给 函数的 prototype 属性值添加新的属性:
function Dog(){
this.run = function () {
console.log('run...')
}
}
Dog.prototype.eat = function (){
console.log('eat...')
}
const mydog = new Dog()
console.log(Dog.prototype)
console.log(mydog.__proto__)
mydog.eat()
mydog.run()
new 运算符创建一个用户定义的对象类型的实例,或具有构造函数的内置对象的实例。new 关键词会进行以下操作:
- 创建一个空的简单对象(即 {})
- 链接该对象(即设置该对象的构造函数) 到另一个对象
- 将步骤1新创建的对象作为 this 的上下文
- 如果该函数没有返回对象,则返回 this
function Person (str){
this.name = str
}
const me = new Person('puxiao')
以上面代码中 new 为例,分别对应上面 4 个环节:
let obj = {}
obj.__proto__ = Person.prototype
Person.apply(obj,'puxiao')
return this
let me = obj
JS 不是 静态编译型 语言,而是 动态解释型 语言。
闭包是指有权访问另外一个函数作用域内变量的函数。
创建闭包的最常见方式就是在一个函数内创建另外一个函数。
-
使我们可以在函数外部访问到函数内部的变量。具体做法是 通过调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
-
使已经运行结束的函数上下文中的变量对象继续保留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量才不会被回收。
function myFun(){ let n = 0 function add(){ n ++ console.log(n) } return add } const add = myFun() console.log(add()) // 1 console.log(add()) // 2
使用 var、let、const 定义变量 或 function 定义函数,默认都会进行变量提升。
JS 中执行上下文是可以改变的,存在以下 2 种情况:
- 定义时的上下文
- 运行时的上下文
作用域:作用域是定义变量的区域,有一套访问变量的规则。分为全局作用域、函数作用域、ES6以后出现的块级作用域
在 ES6之前,JS 不存在块级作用域,在 ES6以后,可以使用 let 来定义块级作用域中的变量。
当代码需要调用某个变量时,若当前作用域中不存在该变量,则会一级一级向外层作用域查找。
这种一级一级查找变量也就形成了一种 “链式结构”,这就是作用域链。
JS 中函数调用有 4 种模式:
- 方法调用
- 正常函数调用
- 构造函数调用
- apply/call调用
无论哪种函数调用,除了声明时定义的形参外,还会自动添加2个形参:this 和 arguments
this 指向谁?
- 在浏览器中,this 指向 windows 对象
- 在函数中,this 指向最后调用它的那个对象
- 在构造函数中,this 指向 new 出来的那个新对象
- call/apply/bind 中的 this 指向被强行绑定的那个对象
- 箭头函数中的 this,指向父作用域中的 this
上面前 4 种情况,this 只有在执行时才能确认值,定义时无法确认。唯独第 5 种情况,this 在定义时就已经被明确下来。
myFun.xx.call(xxx) 中,call(xxx) 可以将 myFun.xx 中的 this 对象修改为 xxx
apply/call 主要区别仅在 传递函数形参时的形式,apply 是用数组,而 call 使用 逗号分隔。
bind 则表示将作用域固定为某对象,但并不会让函数立即执行。
闭包的应用场景:函数返回值依然是函数
所谓同步,就是会阻塞进程,当前不执行完毕不会执行后面代码。
所谓异步,就是不会阻塞进程。
通常情况下,需要 “等待” 的操作需求,都最好使用异步。
例如 加载资源、请求数据、计时器等
alert() 是同步,会阻塞进程
在网页中,有3个标签是可以允许跨域加载资源:
<img\>
<link\>
<script>
所谓通过 JSONP 解决跨域,本质上就是利用了 <script>
标签允许跨域加载资源的特性 来实现跨域数据请求:
- 客户端通过
<script src='xxxxx'>
来请求 某网络 JS 文件地址 - 服务器 动态生成 对应的 JS 文件内容(包含客户端需要的数据)返回给客户端
服务端返回数据时,添加 允许跨域的 header 标签:
respose.setHeader('Access-Control-Allow-Origin','xxxxx')
提交数据内容中包含 <script> 代码,若对方并没有处理,则对方代码中就会包含攻击方的 JS 脚本。
通常 JS 脚本可以获取 cookie 并发送到自己服务器中。
后端解决方案:将 < 替换为 <
、> 替换为 >
这样就不是 JS 代码可执行的片段代码了。
假设某网站支付的接口中,并没有进行用户验证。那么攻击者可以将该请求地址 伪装 隐藏到 给你的特定邮件或网页中,当你点击请求包含该 请求连接接口地址时,则进行自动支付(假设支付接口并不进行二次验证)。
后端解决方案:增加验证流程即可
GET 数据位于 消息头中,而 POST 数据位于 消息体 中。
另外,POST 请求不可以做以下事情:
- POST 请求不会被缓存
- POST 请求不会保留在浏览器历史记录中
- POST 请求不能收藏为书签
- POST 请求对数据长度没有要求限制
一个模块是实现一个特定功能的一组方法,最常用的是立即执行函数的写法,通过利用闭包来实现变量的私有化,不会对全局造成污染。
通过编写纯函数,避免共享状态、可变数据、副作用 来构建软件的过程。
面向对象中应用程序的状态通常和方法共享和共处,但函数式编程不是这样,因此函数式编程代码更简洁,更可预测,更容易测试。
给固定的参数,返回结果也一定是固定值。
将函数作为参数或者返回值的函数,称为高阶函数。
分析:
map() 函数本身作用是将数组中每一个元素都执行一遍某个函数,并将所有的执行结果汇总成一个新的数组。
实现:
function map(arr, callback) {
if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function') {
return []
}
const result = []
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i], i, arr))
}
return result
}
const arr = [0, 1, 2]
function add(n, i, arr) {
return n + 1
}
let new_arr = map(arr, add)
console.log(new_arr) //[1,2,3]
分析:
filter() 函数的作用是将数组中的所有元素都进行一次 某个函数 计算是否匹配,并将所有匹配的元素汇总成一个新数组并返回。
实现:
和上面 map() 的方法类似,唯一区别就在于 map 是 result.push(callback(arr[i],i,arr)),而 filter 应该是:
if(callback(arr[i],i,arr)){
result.push(arr[i])
}
分析:
reduce() 函数的作用是将数组中所有元素都执行一次 某个函数,并依次累加这些计算结果,将最终结果汇总为单个返回值。
实现:
和前面两个略有不同的地方在于 reduce() 函数有一个 初始值 的参数(不传该值则默认为空,不会加入到汇总的起始中)
function reduce(arr, callback, initial) {
if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function') {
return []
}
let boo = initial !== undefined
let result = boo ? initial : arr[0]
for (let i = boo ? 1 : 0; i < arr.length; i++) {
result = callback(result, arr[i], i, arr)
}
return result
}
const arr = [0, 1, 2]
function callbackFun(value, item, i, arr) {
return value += item
}
let value = reduce(arr, callbackFun)
let value2 = reduce(arr, callbackFun, 2)
console.log(value, value2) // 3,5
先说一下浅拷贝:浅拷贝的原则是:
- 若属性为值类型,则直接赋值
- 若属性为引用类型,则添加引用(仅仅是添加指针,内存中并未增加新的存储实体)
第1种浅拷贝:
对于对象来说,比较简单的浅拷贝方式就是:
let b = {...a}
对于组来说,:
let b = [...a]
第2种浅拷贝:
let b = Object.assign({},a)
注意:Object.assign() 仅仅可以实现第一层的拷贝,如果嵌套多层则还是引用而非深度拷贝。
深拷贝
深拷贝就是在内存中,完全新建一份存储实体对象。
深拷贝实现思路为:
- 判断拷贝对象的类型,若为值类型,则直接赋值
- 若为引用类型,则通过递归,深层次对属性进行查询,直到属性的值为值类型后,赋值,逐层赋值。
简单的实现代码:
function deepClone(target) {
let result = undefined
if (target === null || !(typeof target === 'object')) {
result = target
} else {
result = target.constructor.name === 'Array' ? [] : {}
for (let key in target) {
result[key] = deepClone(target[key])
}
}
return result
}
在最新的 JS for in 中,本身就只会循环 对象自身特有的属性,因此是不需要添加 target.hasOwnProperty(key) 判断该属性是自身属性还是原型对象的属性。
防抖 指 在 事件被触发 N 秒后再执行回调函数,如果在这 N 秒内又被触发,则重新计时。
例如在用户信息提交时,可以避免用户多次点击向后台多次提交
function debounce(fun,wait){
let timer
return (...args) => {
clearTimeout(time)
timer = setTimeout(() => {
fun(...args)
}, wait)
}
}
节流 指 在 N 秒之内,无论调用多少次,函数都只执行1次。
例如在滚动浏览器时,scroll 函数的事件监听上,通过节流降低事件调用的频率
function throttle(fun,wait){
let timer
return (...args) => {
if(timer){
return
}
timer = setTimerout(() => {
fun(...args)
timer = null
}, wait)
}
}
函数式编程是一种编程范式,主要利用函数把运算过程封装起来,通过组合各种函数来计算结果。
试想一下以下场景:
将字符串 “hello yang puxiao” 变成每个单词(拼音) 首字母大写。
代码一:按照正常的运算过程,代码为
let str = 'hello yang puxiao'
let arr = str.split(' ') //先用空格将字符串每个单词断开,将单词组合成一个数组
arr = arr.map(item => item.slice(0,1).toUpperCase() + item.slice(1)) //得到新的数组元素
str = arr.join(' ') //将数组元素以空格重新分割,得到最终目标结果
代码二:上面代码可以简写合并为以下代码
let str = 'hello yang puxiao'
str = str.split(' ')
.map(item => item.slice(0,1).toUpperCase() + item.slice(1))
.join(' ')
观察并留意以上 2 种方式的代码,这里先不讨论 代码一,而是去讲一下代码二。
先说结论:代码二 实际上执行的就是 函数式编程思想。
代码二实际上执行流程是:join( map( split( str ) ) ),体现了函数式编程的核心思想:通过函数对数据进行转换。
函数式编程的两个基本特点:
- 通过函数来对数据进行转换
- 通过串联多个函数来求结果
命令式: 通过编写一条又一条的指令去计算执行一些动作。通常会涉及一些复杂的细节和语句,例如 for、if、switch、throw
声明式: 通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。表达式通常是某些函数调用的复合,一些值和操作符,用来计算出结果值。
//命令式
const team = [{ceo:'a'},{ceo:'b'},{ceo:'c'}]
const ceo = []
for(let i = 0; i< team.length; i++){
ceo.push(team[i].ceo)
}
//声明式
const team = const team = [{ceo:'a'},{ceo:'b'},{ceo:'c'}]
const ceo = team.map(item => item.ceo)
从上面代码对比中,可以看到,命令式更加强调执行过程中的每一个细节。而声明式则仅为一个表达式,不关心计数器迭代过程、不关心返回的数组如何收集。
命令式强调的是:每一步怎么做,并且每一个步骤都会记录中间结果,方便调试
声明式强调的是:做什么,但不过分关注直接过程
函数式编程就是声明式编程中的一种。
函数式编程主要思想是将计算机运算看作函数的计算。也就是把程序问题抽象成数学问题去解决。
函数式编程中,所有的变量都是唯一的值,就像是数学中的代数 x y,他们要么还未被解出,要么是被解出的固定值。对于 x = x +1 这种自增是不合法的,因为修改了代数值,不符合数学逻辑。
严格意义上讲,函数式编程中不可以出现 if、switch 等控制语句,如果需要条件判断可使用三元运算符。
无副作用指调用函数时不会修改外部状态,即一个函数调用 N 次后依然返回相同的结果。通常把这种无副作用的函数称为 纯函数。
let num = 0
//每次执行 add(),都会修改外部的 num 的值,因此 add() 函数不是纯函数,他是有副作用的
function add(){
num ++
return num
}
//每次执行 add2(),函数内部执行的是将参数 num + 1 并返回该值,并没有修改外部任何对象
//无论执行多少次 add2(),只要保证所传入的 参数一致,其返回结果也一定相同
//因此这里的 add2() 是纯函数,且无副作用。
function add2(num) {
return num + 1
}
透明引用指函数只会用到传递给他的变量(参数)以及内部创建的变量,不会使用到其他变量。
let a = 1
let b = 2
//add() 函数内部使用到了本不是自己内部定义,也不是传递进来的参数的变量
function add(){
return a + b
}
//add2() 函数内部仅使用到了内部定义或参数变量,没有用到其他变量
function add2(a,b){
return a + b
}
函数式编程采用透明引用,除了参数外,内部不引用其他外部变量。
不可变变量指变量一旦创建后,就不能再进行修改。任何修改都会生成一个新的变量。
换句话说:不可以修改变量,但可以返回新的值。
这是一个比喻,所谓一等公民,不是指函数最厉害,而是指 函数和其他一等公民一样(变量),可以赋值给其他变量,也可以作为参数,或者作为函数返回值。
虽然本小节讲的是 函数式编程,但是这里插入一些 Object.freeze() 的知识,尽管和本小节并无任何关联。
注意:const 关键词声明的变量,仅仅表示类型不可变,类型的某属性值是可以修改的。
因此使用 const 声明变量并不能满足 不可变变量 的要求。
const obj = { age: 18 }
obj.age = 34
console.log(obj.age) // 34
在 ES6 之前,原生 JS 不支持不可变变量,需要使用第三方库来实现,例如 Immutable.js、Mori 等。
在 ES6 之后,通过新增的 Object.freeze() 来冻结一个对象,来实现 不可变变量。
const obj = Object.freeze({ age: 18 })
obj.age = 34 //在JS严格模式下,此时会抛出一个 TypeError 错误
console.log(obj.age) //18
通常情况下,不可变变量 会应用在 配置项中,这样避免其他地方无意修改配置项。
使用 Object.freeze() 冻结对象的补充说明:
补充1:冻结对象后
- 不能添加新属性
- 不能删除已有新属性
- 不能修改已有属性的值
- 不能修改原型
- 不能修改已有属性的可枚举新、可配置性、可写性
补充2:默认只冻结对象第一层属性
若对象某属性依然为一个对象,则第二层对象属性值是不受约束,是可以修改的。
let me = Object.freeze({age:18,do:{react:'React'}})
me.do.react = 'Taro'
console.log(me.do.react) // Taro
补充3:属性访问器 getter 和 setter 不受影响
假设该对象具有属性访问器 getter 和 setter,那么他们作为函数本身也是会被 冻结,无法再修改的,但由于它们是函数,且可以通过参数修改属性值,所以会给人错觉,以为还是可以修改属性值的。
补充4:若数组被冻结,则无法向该数组增加或删除数组元素
补充5:通过递归,深层冻结
function deepFreeze(obj){
for( let key in obj){
if(obj[key] !==null && typeof obj[key] === 'object'){
deepFreeze(obj[key])
}
}
return Object.freeze(obj)
}
const me = deepFreeze({age:18,do:{react:'React'}})
me.do.react = 'Taro' //在严格模式下,会报 TypeError 错误
console.log(me) //React
补充6:若使用 let 创建变量,尽管引用对象可以被冻结无法修改,但可以对变量重新赋值
//若使用 let 来定义变量
let me = Object.freeze({age:18})
me = {age:34} //直接将变量重新赋值
console.log(me.age) //34
//若使用 const 来定义变量
const me = Object.freeze({age:18})
me = {age:34} //尝试将变量重新赋值,会直接报错
自由变量指不属于函数作用域的变量,通常情况下是指外层函数内定义的变量。
所有全局变量都是自由变量,严格来说引用了全局变量的函数都是闭包,但这种闭包并没有什么实际意义作用,通常情况下自由变量是指 闭包 中外层函数中定义的变量。
闭包的形成条件:
- 函数内创建函数,即存在 内、外 两层函数
- 内层函数中引用了外层函数中定义的局部变量
闭包的用途:可以定义写作用域局限的持久化变量,这些变量可以用来做缓存或者计算中间量等。
简单来说,就是通过闭包,可以将函数内定义的变量持久化
闭包将局部变量持久化的代码示例:
//通过匿名函数,创建了一个闭包
const cache = (function(){
const store = {}
return {
get(key){
return store[key]
}
set(key,val){
store[key]=val
}
}
}())
cache.set('num':1)
cache.get('num') //1
请注意:上述代码中,通过创建闭包,持久化了一个局部变量 sotre,弊端是 sotre 持续占用内存空间,永远不会被正常释放(垃圾回收),容易造成内存浪费,所以一般需要一些额外手动的清理机制。
高阶函数:指一个函数以函数为参数,或以函数为返回值,再或者即以函数为参数同时返回值也是一个函数。
高阶函数常见用途:
- 抽象或隔离行为、作用、异步控制流程作为回调函数
- 创建可以泛用于各种数据类型的功能
- 部分应用于函数参数,或 创建一个柯里化的函数
- 接收一个函数列表,并返回一些由这个列表中的函数组成的复合函数
以上 4 条总结摘抄于 网上相关教程,本人暂时不太完全理解。
使用高阶函数会让我们的代码更加清晰简洁。例如 Array.prototype.map 就是高阶函数。
柯里化又称部分求值,柯里化函数会接收一些参数,然后不会立即求值,而是继续返回一个新函数,将传入的参数通过闭包的形式保存,等到被真正求值的时候,再一次性把所有传入的参数进行求值。
//普通函数
function add(x,y){
return x + y
}
add(1,2) //3
//柯里化函数
const add = function(x) {
return function (y) {
return x + y
}
}
let increment = add(1)
imcrement(2) //3
以上代码还可以简化为:
//柯里化函数
const add = function(x) {
return function (y) {
return x + y
}
}
let result = add(1)(2)
console.log(result) //3
柯里化通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的 “缓存”,是一个高效的编写函数的方法。
依我目前的理解,柯里化本质上是运用了 JS 中函数参数数量不做严格限制的前提下进行的,我认为在 TypeScript 环境下,不太适合做柯里化。