JavaScript常规函数

原创
2023/08/27 01:31
阅读数 36

一般来说,函数是一个“子程序”,可以由函数外部(或内部,在递归的情况下)代码调用。与程序本身一样,函数由一系列称为函数体的语句组成。值可以作为参数传递给函数,函数将返回一个值。

在 JavaScript 中,函数是一等对象(first-class objects),因为它们可以传递给其他函数、从函数返回以及分配给变量和属性。它们也可以像任何其他对象一样具有属性和方法。它们与其他对象的区别在于可以调用函数。

函数声明

function 声明创建新函数与给定名称的绑定,还可以使用 function 表达式定义函数。默认情况下,函数是返回 undefined 的。想要返回一个其他的值,函数必须通过一个 return 语句指定返回值。

function add(a, b) {
  return a + b
}

function 声明可能出现在一个 if 语句里,但是,这种声明方式在不同的浏览器里可能有不同的效果。因此,不应该在生产环境代码中使用这种声明方式,应该使用函数表达式来代替。

if (false) {
  function add(a, b) {
    return a + b
  }
}

JavaScript 中的 function 声明被提升到封闭函数或全局作用域的顶部,可以在声明之前使用该函数:

hoisted()  // 'hoisted'

function hoisted() {
  console.log('hoisted')
}

函数表达式

函数表达式可以使用 function 声明,也可以使用 Function 构造函数,也可以使用箭头函数来定义。在函数表达式中可省略函数名称,从而创建匿名函数。

const fn = function () {}

function 声明不同,JavaScript 中的函数表达式不会被提升,在创建函数表达式之前不能使用它们。

notHoisted()  // TypeError: notHoisted is not a function

let notHoisted = function () {
  console.log('notHoisted')
}

如果要在函数体内引用当前函数,则需要创建一个命名函数表达式。该名称仅适用于函数体(作用域)。这可以避免使用已弃用的 arguments.callee 属性来递归调用该函数。

const math = {
  factit: function factorial (n) {
    return n <= 1 ? 1 : n * factorial(n - 1)
  }
}

math.factit(3)

如果函数表达式被命名,则函数的 name 属性将设置为该名称,而不是从语法推断的隐式名称(例如函数分配给的变量)。与 function 声明不同,函数表达式的名称是只读的。

function fn () {
  fn = 1
  console.log(fn)  // 1
}

fn()

!(function fn () {
  fn = 1
  console.log(fn)  // ƒ fn
})()

箭头函数表达式

箭头函数是传统函数表达式的紧凑替代品,更适用于那些本来需要匿名函数的地方,具有一些语义差异和使用方面的故意限制:

  • 箭头函数没有自己的 thisargumentssuper 绑定,因此不应用作方法。
  • 箭头函数不能用作构造函数。使用 new 调用它们会抛出 TypeError 。他们也无权访问 new.target 关键字。
  • 箭头函数不能在其主体内使用 yield ,也不能创建为 generator 函数。
  • 箭头函数的参数和箭头之间不能包含换行符。
const fn = () => {}

箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this

function Person() {
  this.age = 0

  setInterval(() => {
    ++this.age  // this -> p: Person {age: 1}
  }, 1000)
}

const p = new Person()

鉴于 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。

let f = () => {
  'use strict'
  return this
}
// f() === window

由于箭头函数没有自己的 this 指针,通过 call()apply() 方法调用一个函数时,只能传递参数,他们的第一个参数会被忽略。

const plus = {
  base: 1,
  add: function (n) {
    return ((v) => v + this.base)(n)
  },
  addThruCall: function (n) {
    return ((v) => v + this.base).call({ base: 2 }, n)
  }
}

console.log(plus.add(1))  // 2
console.log(plus.addThruCall(1))  // 2

箭头函数不绑定 arguments 对象,支持剩余参数、默认参数和参数内的解构,并且始终需要括号:

const fn = (z, ...[x = 20, y = 30]) => x + y + z
console.log(fn(10))  // 60
console.log(fn(40, 50, 60))  // 150

箭头函数可以通过在表达式前加上 async 关键字来实现 async

const asyncFn = async (x) => x * x
asyncFn(10).then(console.log)  // 100

箭头函数具有简洁的函数体或通常的块体。在简洁的主体中,仅指定单个表达式,该表达式成为隐式返回值。在块体中,您必须使用显式的 return 语句。使用简洁的正文语法 (params) => { object: literal } 返回对象字面量无法按预期工作。要解决此问题,请将对象字面量括在括号中:

const fn = () => ({ foo: 1 })

尽管箭头函数中的箭头不是运算符,但箭头函数具有特殊的解析规则,与常规函数相比,它们与运算符优先级的交互方式不同。由于 => 的优先级低于大多数运算符,因此需要括号以避免 callback || () 被解析为箭头函数的参数列表。

let callback
// callback = callback || () => {}
// SyntaxError: invalid arrow-function arguments

callback = callback || (() => {})

立即调用函数表达式

立即调用函数表达式(IIFE)是一个在定义时就会立即执行的 JavaScript 函数。它是一种设计模式,也被称为自执行匿名函数,主要包含两部分:

  • 第一部分是匿名函数,其词法范围包含在 Grouping Operator () 内。这可以防止访问 IIFE 惯用法中的变量以及污染全局范围。
  • 第二部分创建立即调用的函数表达式 () ,JavaScript 引擎将通过该表达式直接解释该函数。

我们可以使用 IIFE 来创建私有和公有变量、方法,避免污染全局命名空间。

!(() => {
  let x = 10
})()

IIFE 允许在比较旧的浏览器或者 JavaScript 运行环境没有顶层 await 中使用 awaitfor-await

const getFileStream = async (url) => {}

!(async () => {
  const stream = await getFileStream('https://domain.com/file.excel')

  for await (const chunk of stream) {
    console.log({ chunk })
  }
})()

在 ES2015 引入 letconst 声明和块级作用域之前,通过 var 声明变量,只有函数作用域和全局作用域。

for (var i = 0; i < 2; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i)
    }, 1000)
  })(i)
}

构造函数

一个被 function 声明创建的函数是一个 Function 对象,具有 Function 对象的所有属性、方法和行为。直接调用构造函数可以动态创建函数,但会遇到安全性和与 eval() 类似的性能问题。但是,与 eval 不同, Function 构造函数创建仅在全局作用域中执行的函数。

Function('a', 'b', 'return a + b')

除最后一个参数,传递给函数的所有参数都被视为要创建的函数中参数的标识符名称,按照传递的顺序排列。该函数将动态编译为函数表达式,源代码按以下方式组装:

// ƒ anonymous(a,b
// ) {
// return a + b
// }

使用 Function 比使用函数表达式或 function 声明创建函数并在代码中调用它的效率要低,因为此类函数是与代码的其余部分一起解析的。组装源代码的两个动态部分: 参数列表 args.join(',')functionBody。它们将首先单独解析,以确保它们在语法上都是有效的,可以防止类似注入的尝试。

`ƒ anonymous(${args.join(',')}
) {
${functionBody}
}`

与普通函数表达式不同,名称 anonymous 不会添加到 functionBody 的作用域中,因为 functionBody 只能访问全局作用域。如果 functionBody 不是严格模式,可以使用 arguments.callee

闭包

闭包(closure)是一个函数与其周围状态(lexical environment,词法环境)的引用捆绑在一起(封闭)的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

function init () {
  let name = 'Mozilla'

  return function () {
    console.log(name)
  }
}

const fn = init()
fn()

实用的闭包

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。因此,通常使用只有一个方法的对象的地方,都可以使用闭包。

function setBaseSize (size) {
  return function () {
    document.body.style.fontSize = `${size}px`
  }
}

document.getElementById('size60').onclick = setBaseSize(60)
document.getElementById('size50').onclick = setBaseSize(50)

我们可以使用闭包来模拟私有方法,私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

const Counter = (function() {
  let num = 0

  function change (v) {
    num += v
  }

  return {
    increment () {
      change(1)
    },
    decrement () {
      change(-1)
    },
    value () {
      return num
    }
  }
})()

Counter.increment()
Counter.increment()
console.log(Counter.value()) // 2
Counter.decrement()
console.log(Counter.value()) // 1

在循环中创建闭包

在 ECMAScript 2015 引入 let 关键字 之前,在循环中有一个常见的闭包创建问题。

var actions = []

function setup () {
  for (var i = 0; i < 3; i++) {
    actions[i] = function () {
      console.log(i)
    }
  }
}

setup()

for (let i = 0; i < 3; i++) {
  actions[i]()
}

结果输出的全是 3,解决这个问题的一种方案是使用更多的闭包:

// 函数工厂
function insert(action) {
  return function () {
    console.log(action)
  }
}

function setup() {
  for (var i = 0; i < 3; i++) {
    actions[i] = insert(i)
  }
}

工厂函数为每一个回调创建一个新的词法环境,结果正确输出。另外一种解决方案是使用 IIFE:

function setup() {
  for (var i = 0; i < 3; i++) {
    (function (n) {
      actions[n] = function () {
        console.log(n)
      }
    })(i)
  }
}

除此之外,还可以利用数组的方法 mapforEach的回调函数形成独立的词法环境。ES2015 引入的 letconst 关键词可以用来解决这个问题。

性能考量

如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

function P (name) {
  this.name = name

  this.getName = function () {
    return this.name
  }
}

上面的代码并没有利用到闭包的好处,相反,当生成实例还会产生更多副本。因此可以避免使用闭包,利用原型继承共享对象方法,但不建议重新定义原型。

function P (name) {
  this.name = name
}

P.prototype.getName = function () {
  return this.name
}

在用完闭包及时手动解除引用,避免产生内存泄漏。

function fn() {
  const o = {
    num: 1
  }
  return function () {
    console.log(o.a)
  }
}

let f = fn()
f()
f = null
展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部