js函数进阶、this与闭包的深入理解

574 阅读6分钟

函数的灵活

当参数

函数的参数可以传函数

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

image-20220303210829814.png

因为bar()创建的AO对象还有对父级作用域(也就是foo())的引用指向

但是,bar()被GO的成员指向了,这样只要GO对象不销毁,那bar()内存空间就会一直占用,要是一直使用fn()还好,但我们只想要执行一次fn()的话就存在内存泄露了!不需要fn()了但它不被销毁

所以说闭包可能会产生内存泄漏,取决于你要不要继续使用那个函数对象

解决闭包产生的内存泄漏

怎么解决?

fn = null就行了

image-20220303211422577.png

按照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啦