函数的灵活
当参数
函数的参数可以传函数
function calc(num1, num2, calcFn) {
console.log(calcFn(num1, num2))
}
function add(n1, n2) {
return n1 + n2
}
function decrease(n1, n2) {
return n1 - n2
}
var num1 = 30
var num2 = 20
calc(num1, num2, add)//50
calc(num1, num2, decrease)//10
当返回值
function foo() {
function bar() {
console.log("bar")
}
return bar
}
var fn = foo()
fn()//bar
高阶函数
接收另外一个函数为参数,或者返回值是另一个函数时,这种函数叫高阶函数
js常见高阶函数:
数组的
- filter()
- map()
- forEach()
- reduce()
- find()
- findindex()
纯函数
掌握纯函数对于理解很多框架的设计是非常有帮助的
纯函数特点:
- 确定的输入,一定会产生确定的输出
- 不能产生副作用
副作用
副作用(side effect)
执行一个函数时,除了返回值以外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
副作用,往往是产生bug的温床!
纯函数举例
- slice 截取数组不会对原函数进行任何操作,而是产生一个新的数组(不会修改传入的参数)
而splice 截取数组,返回新数组,同时也会对原数组进行修改
var list = ['a', 'b', 'c']
var newList = list.slice(0,2)
console.log(newList)// ['a', 'b']
console.log(list)// ['a', 'b', 'c']
var list = ['a', 'b', 'c', 'd']
var newList = list.splice(2)
console.log(newList)// ['c', 'd']
console.log(list)// ['a', 'b']
这也是一个纯函数
var obj = {
name: 'zsf',
age: 18
}
function foo(info) {
return {
...info,
age: 100
}
}
foo(obj)
console.log(obj)
...info拿的只是obj的一个副本,并没有修改obj
react组件要求是一个纯函数
优势
为什么纯函数在函数式编程中非常重要呢?
- 纯函数可以安心的编写和安心的使用,只需要关心参数和返回值
- 让函数的职责单一
- 可以进行逻辑的复用,复用之后可以定制一些东西,拓展性强
柯里化逻辑复用例子
// 假如有这样一个需求: 把5和另外的一个数字相加
console.log(sum(5, 10))
console.log(sum(5, 149))
console.log(sum(5, 106))
console.log(sum(5, 199))
// 柯里化的可以进行逻辑的复用,参数5多次使用,可以复用
function makeAdder(count) {
return function (num) {
return count + num
}
}
var adder5 = makeAdder(5)
adder5(10)
adder5(149)
adder5(106)
adder5(199)
柯里化Currying
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就叫柯里化
只要是将多个参数的一次函数调用拆分成多次函数调用
function foo(a, b, c) {
return a + b + c
}
foo(1, 2, 3)
// 柯里化
function bar(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
bar(1)(2)(3)
简化柯里化
var bar = a => b => c => {
return a + b + c
}
实现柯里化
传入一个函数,转成已经柯里化的函数
function add(a, b, c) {
return a + b + c
}
function sfCurrying(fn) {
// 1.传入一个函数,返回一个函数
function curried(...args) {
// 2.需要判断该接收的参数是否接收完毕
// 2.1 如何知道传入函数需要多少参数 函数名.length
if (args.length >= fn.length) {
// 使用apply防止被人调用时修改this指向,导致错误
return fn.apply(this, args)
} else {
// 参数还不够,返回新函数
return function curried2(...args2) {
// 使用递归,拼接参数
return curried.apply(this, [...args, ...args2])
}
}
}
return curried
}
// 对add柯里化
var curryingFn = sfCurrying(add)
// add(10, 20, 30)
console.log(curryingFn(10, 20, 30))
console.log(curryingFn(10, 20)(30))
console.log(curryingFn(10)(20)(30))
应用场景
vue3源码的渲染器里的最后用到
组合函数
组合函数是js开发过程中一种对函数使用的技巧,模式
当一个数据需要依次调用多个函数时可以应用这个技巧
例子
function double(n) {
return n * 2
}
function square(m) {
return m ** 2
}
// 需求:square(double(count))
function composFn(fn1, fn2) {
return function (count) {
// 取决于调用顺序
return fn2(fn1(count))
}
}
var newFn = composFn(double, square)
console.log(newFn(10))//400
通用组合函数的实现
function double(n) {
return n * 2
}
function square(m) {
return m ** 2
}
// 需求:通用组合函数
function sfCompos(...fns) {
// 边界情况1 参数传入非函数
var length = fns.length
for (let i = 0; i < length; i++) {
if (typeof fns[i] !== 'function') {
throw new TypeError('期望参数是函数')
}
}
return function compos(...args) {
var index = 0
var result = length ? fns[index].apply(this, args): args
while (++index < length) {
result = fns[index].call(this, result)
}
return result
}
}
var newFn = sfCompos(double, square)
console.log(newFn(10))//400
错误处理
抛出异常
调用一个函数时,如果出现了错误,应该去修复这个错误~
不然返回结果不是预期结果
当函数出现错误时,应该告诉调用者出现了什么错误
比如
function sum(num1, num2) {
if (typeof num1 !== 'number' || typeof num2 !== 'number') {
throw 'error'
}
return num1 + num2
}
console.log(sum({name: 'zsf'}, 'hhh'))
当参数不是number类型,就告诉调用者错误,如果调用者不进行错误处理,就终止程序
一般抛出的是对象,能放更多信息
Error类
function sum(num1, num2) {
if (typeof num1 !== 'number' || typeof num2 !== 'number') {
throw new Error('不能传入非数字类型~')
}
return num1 + num2
}
console.log(sum({name: 'zsf'}, 'hhh'))
属性
- message
- name
- stack
stack可以看到函数调用栈
function foo(params) {
throw new Error('error')
}
function bar(params) {
foo()
}
function test(params) {
bar()
}
function demo(params) {
test()
}
demo()
// Uncaught Error: error
// at foo
// at bar
// at test
// at demo
// at index.js:17:1
Error的子类
- TypeError 类型错误
- **RangeError **下标越界
- **SyntaxError **语法错误
处理异常
不处理
会将异常抛给调用者,最终会抛给顶层调用者,如果顶层不处理,程序终止,并且报错
function foo(params) {
throw new Error('error')
}
function bar(params) {
foo()
}
function test(params) {
bar()
}
function demo(params) {
test()
}
demo()
console.log('后续代码')
捕获异常
在可能出现异常的代码片段使用try...catch
function foo(params) {
throw new Error('error')
}
function bar(params) {
try {
foo()
} catch (error) {
console.log(error)
}
}
function test(params) {
bar()
}
function demo(params) {
test()
}
demo()
console.log('后续代码')
处理了异常--打印异常信息,
后续代码正常执行~
finally
类似promise的finally,不管有没有异常都会执行里面代码
闭包
定义
计算机科学中的闭包
js中的闭包
function foo() {
var name = 'zsf'
function bar() {
console.log("bar", name)
}
return bar
}
var fn = foo()
fn()//bar
闭包=bar()+name
闭包可以理解为函数+可以访问的自由变量
**执行完foo()**后,理应销毁foo()的作用域,但是foo()**内部的的bar()**还需要访问
foo()作用域中的name(阻止了foo()的回收)
广义上,js中所有函数都是闭包(可以访问外层作用域的自由变量)
狭义上,js中的函数如果访问了外层作用域的变量(访问了),那这函数就是一个闭包
闭包产生的问题
本应该foo()应该销毁,但是闭包阻止了foo()的回收,为什么?
function foo() {
var name = 'zsf'
function bar() {
console.log("bar", name)
}
return bar
}
var fn = foo()
fn()//bar
因为bar()创建的AO对象还有对父级作用域(也就是foo())的引用指向
但是,bar()被GO的成员指向了,这样只要GO对象不销毁,那bar()内存空间就会一直占用,要是一直使用fn()还好,但我们只想要执行一次fn()的话就存在内存泄露了!不需要fn()了但它不被销毁
所以说闭包可能会产生内存泄漏,取决于你要不要继续使用那个函数对象
解决闭包产生的内存泄漏
怎么解决?
fn = null就行了
按照GC的清楚标记算法,由于bar()不可达,虽然bar对象和foo对象存在循环引用,但也会销毁
补充一点
function foo() {
var name = 'zsf'
var age = 18
function bar() {
debugger
console.log(name)
}
return bar
}
var fn = foo()
fn()//bar
v8引擎会删掉 var age = 18这行,因为闭包时没有使用age(开发者工具中可观察到-closure)
v8引擎正是做了很多细节的优化,所以性能很高
this
为什么需要this?
没有this,平常写代码很不方便,拷贝一个对象时,很多地方可能都需要修改
this的指向
与函数定义时位置无关,与函数调用时位置有关
function foo() {
console.log(this);
}
// 1.直接调用这函数
foo()//window对象
var obj = {
name: 'zsf',
fn: foo
}
// 2.创建一个对象,对象中的函数指向foo
obj.fn()//obj对象
// 3.apply调用
foo.apply('123')//String对象
绑定规则
默认绑定
独立函数调用,指向window
函数调用时没有调用主题
function foo() {
console.log(this)
}
foo()//window对象
var obj = {
name: 'zsf',
fn: function foo() {
console.log(this)
}
}
var bar = obj.fn
bar()//window,依然没有调用主题
隐式绑定
v8引擎绑定的
函数通过某个对象进行调用的,this绑定的就是该对象
前提条件:
- 必须在调用的对象内部有一个对函数的引用(比如一个属性)
- 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
- 正是通过这个引用,间接的将this绑定到了这个对象上
var obj = {
name: 'zsf',
fn: function foo() {
console.log(this)
}
}
obj.fn()//obj对象
var obj1 = {
name: 'zsf',
fn: function foo() {
console.log(this)
}
}
var obj2 = {
name: 'obj2',
fn: obj1.fn
}
obj2.fn()
显式绑定
如果不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,怎么做?
每个函数对象都有这2个方法
- call()
- apply()
- bind()
function foo() {
console.log("被调用了", this);
}
// 直接调用和call()/apply()调用的区别在于this绑定不同
// 直接调用this指向window
foo()
// call()/apply()调用会指定this绑定对象
var obj = {
name: 'obj'
}
foo.call(obj)//obj
foo.apply(obj)//obj
foo.apply("aaa")//aaa
直接调用和call()/apply()调用的区别在于this绑定不同:
- 直接调用this指向window
- call()/apply()调用会指定this绑定对象
call和apply的区别
传参方式不同,call接收多个参数是以逗号分开,而apply会将多个参数放数组里
function sum(num1, num2) {
console.log(num1 + num2, this)
}
foo.call('call', 20, 30)
foo.apply('qpply', [20, 30])
bind的显示绑定
function foo() {
console.log(this)
}
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')
等价于
function foo() {
console.log(this)
}
// 隐式绑定和显式绑定冲突了,根据优先级,显式绑定
var newFoo = foo.bind('aaa')
newFoo()
newFoo()
newFoo()
newFoo()
bing绑定之后会生成一个新的函数返回
new绑定
js中的函数可以当做一个类的构造函数来使用,也就是使用new关键字
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person('zsf', 18)
console.log(p1)
通过一个new关键字调用函数时(构造器),这个时候this是在调用这个构造器时创建出来的,并且this = 创建出来的对象
规则之外
忽略显式绑定
function foo() {
console.log(this)
}
foo.call(null)
foo.call(undefined)
call、apply、bind当传入参数为null或undefined时,自动将this绑定到window对象
间接函数引用
var obj1 = {
name: 'obj1',
foo: function () {
console.log(this)
}
}
var obj2 = {
name: 'obj2'
};
(obj2.bar = obj1.foo)()//this
(obj1.foo)()//obj1
这属于独立函数调用(无等号就是隐式绑定)
箭头函数的规则
基础语法
箭头函数不会绑定this、arguments属性
箭头函数不能作为构造函数来使用(不能和new关键字一起使用)
- ()
- =>
- {}
简写:
- 只有一个参数可以省()
- 只有一行执行提可以省{},并且会将该行代码当结果返回
- 执行体只有一行且返回的是一个对象,小括号()括起来
var nums = [1,2,3]
nums.forEach(item => {
console.log(item)
})
var nums = [1,2,3]
nums.filter(item => item % 2 === 0)
var bar = () => ({name: 'zsf', age: 18})
规则
箭头函数不绑定this,而是根据外层作用域来决定this
var foo = () => {
console.log(this)
}
foo()//window
var obj = {
fn: foo
}
obj.fn()//window
foo.call('abc')//window
这样有什么应用呢?
在箭头函数出来之前
var obj = {
data: [],
getDate: function () {
// 发送网络请求,将结果放上面的data属性中
var _this = this
setTimeout(function () {
var result = ['abc', 'bbv', 'ccc']
_this.data = result
}, 2000)
}
}
// 由于这里的隐式绑定,第5行的this绑定了obj对象。才有了,第8行的写法
obj.getDate()
箭头函数出来之后
var obj = {
data: [],
getDate: function () {
// 发送网络请求,将结果放上面的data属性中
setTimeout( () => {
var result = ['abc', 'bbv', 'ccc']
this.data = result
}, 2000)
}
}
obj.getDate()
箭头函数不绑定this,相当于没有this,会寻找上层作用域寻找this,也就是在getData的作用域里找this,而obj.getData已经隐式绑定了getData里的this指向obj
一些函数的this分析
setTimeout
setTimeout(function () {
console.log(this)//window
}, 1000)
setTimeout内部使用的独立函数调用,所以this默认绑定window对象
规则优先级
- 默认最低
- 显式高于隐式
- new高于隐式
- new高于显式
var obj = {
name: 'obj',
fn: function foo() {
console.log(this)
}
}
obj.fn.call('abc')//abc
function foo() {
console.log(this)
}
var obj = {
name: 'obj',
fn: foo.bind('abc')
}
obj.fn()//abc
var obj = {
name: 'obj',
fn: function () {
console.log(this)
}
}
var p = new obj.fn()//fn函数对象
function foo() {
console.log(this)
}
var bar = foo.bind('aa')
var p = new bar()//foo
由于call和apply都是主动调用函数,所以不能和new一起使用
实现apply、call、bind
call
补充:
展开运算符...(类似遍历)
var names = ['abc', 'abb', 'ccc']
function foo (n1, n2, n3) {
}
foo(...names)
自己实现
// 给所有函数加上一个自定义call
Function.prototype.sfcall = function (thisArg, ...args) {
// 在这里可以执行调用者(函数)
// 问题1:如何获取到是哪个函数调用了sfcall?
var fn = this
// 边界情况edge case1 对thisArg转成对象类型(防止传入非对象类型报错)
// 边界情况edge case2 传入参数null/undefined
thisArg = thisArg ? Object(thisArg) : window
// 边界情况edge case2 调用者(函数)有一个或多个参数时
// 如何执行调用者(函数)?
thisArg.fn = fn
// 边界情况edge case3 调用者有返回值
var result = thisArg.fn(...args)
delete thisArg.fn
// 返回调用者的返回值
return result
}
function foo(n1, n2) {
console.log('foo执行了', this, n1, n2)
console.log(n1 + n2);
}
foo.sfcall('sss', 1, 2)
apply
有时间再补
bind
有时间再补
补充
arguments
把类数组(array-like)对象arguments转化成array
- Array.prototype.slice.call(arguments)
- Array.from(arguments) es6
- [...arguments]
剩余参数
es6 剩余参数可以替代掉arguments啦