前端进阶-运行时函数

一级函数

函数是一级函数

在 JavaScript 中,函数是一级函数。这意味着,就像对象一样,你可以像处理其他元素(如数字、字符串、数组等)一样来处理函数。JavaScript 函数可以:

  • 存储在变量中
  • 从一个函数返回
  • 作为参数传递给另一个函数

注意,虽然我们可以将函数当作对象来处理,但是函数和对象之间的一个主要区别是,函数可以被调用(即使用 () 执行),而常规对象则不能。

在很多方面,JavaScript 中的函数都可以被当作一个值。你完全可以从一个函数中返回它,将其存储在一个变量中,甚至作为参数传递给另一个函数

函数可以返回函数

函数必须始终返回一个值。无论是在 return 语句中显式指定一个值(例如,返回一个字符串布尔值数组等),还是函数隐式地返回 undefined(例如,一个简单地将某些东西记录到控制台的函数),函数始终只会返回一个值

既然我们知道函数是一级函数,我们可以将函数作为一个值,十分简便地从函数返回函数!返回另一个函数的函数被称为高阶函数。请考虑以下示例:

function alertThenReturn() {
  alert('Message 1!');

  return function () {
    alert('Message 2!');
  };
}

如果在浏览器中调用 alertThenReturn(),我们会先看到一条提示消息,写着 Message 1!,接着是 alertThenReturn() 函数,它会返回一个匿名函数。但是,我们并不会看到一个 Message 2! 提示,因为并未执行内部函数中的任何代码。那么,我们如何执行所返回的函数呢?

由于 alertThenReturn() 会返回一个函数,因此我们可以给这个返回值分配一个变量

const innerFunction = alertThenReturn();

然后,我们可以像使用其他函数一样使用 innerFunction 变量!

innerFunction();
// 显示 'Message 2!'

同样,这个函数可以被立即调用,而无需存储在一个变量中。如果我们简单地向 alertThenReturn() 添加另一组圆括号,我们仍然会得到相同的结果:

alertThenReturn()();
// 显示 'Message 1!' 然后显示 'Message 2!'

请注意这个函数调用中的两对圆括号(即 ()())!第一对圆括号将执行 alertThenReturn 函数。这个调用的返回值将返回一个函数,然后再被第二对圆括号调用!

在这里插入图片描述

回调

回调函数

JavaScript 函数是一级函数。我们可以像处理其他值一样来处理函数——包括将它们传递给其他函数!接受其他函数作为参数(和/或返回函数,如上一部分所述)的函数被称为高阶函数。作为参数传入其它函数中的函数被称为回调函数接收函数可以在运行自己的代码后执行回调函数

关于回调:

  • 作为参数传递给另一个函数的函数被称为回调函数
  • 接受另一个函数作为参数的函数是一个高阶函数
  • 我们可以利用回调函数,因为 JavaScript 函数是一级函数

数组方法

forEach()

数组的 forEach() 方法接受一个回调函数,并为数组中的每个元素调用该函数。换句话说,forEach() 让你可以迭代(即遍历)一个数组,类似于使用 for 循环。

array.forEach(function callback(currentValue, index, array) {
	// 回调函数本身会接收参数:当前数组元素、其索引和整个数组本身。
    // 函数代码写在这里
});

将一个匿名函数作为参数传递给 forEach() 是很常见的:

[1, 5, 2, 4, 6, 3].forEach(function logIfOdd(n) {
  if (n % 2 !== 0) {
    console.log(n);
  }
});

[1, 5, 2, 4, 6, 3].forEach(function (n) {
  if (n % 2 !== 0) {
    console.log(n);
  }
});

// 1
// 5
// 3

另外,也可以简单地传入函数的名称(当然,假设函数已经被定义了)。

[1, 5, 2, 4, 6, 3].forEach(logIfOdd);

// 1
// 5
// 3

map()

数组的 map() 方法类似于 forEach(),也会为数组中的每个元素调用一个回调函数。但是,.map() 会根据回调函数所返回的内容返回一个新的数组

const names = ['David', 'Richard', 'Veronika'];

const nameLengths = names.map(function(name) {
  return name.length;
});

请记住,.forEach().map() 之间的主要区别在于,.forEach() 不会返回任何东西,而 .map() 则会返回一个新的数组,其中包含从该函数返回的值。

map() 方法会返回一个新的数组,而不会修改原始数组。

map()

const musicData = [
    { artist: 'Adele', name: '25', sales: 1731000 },
    { artist: 'Drake', name: 'Views', sales: 1608000 },
    { artist: 'Beyonce', name: 'Lemonade', sales: 1554000 },
    { artist: 'Chris Stapleton', name: 'Traveller', sales: 1085000 },
    { artist: 'Pentatonix', name: 'A Pentatonix Christmas', sales: 904000 },
    { artist: 'Original Broadway Cast Recording', 
      name: 'Hamilton: An American Musical', sales: 820000 },
    { artist: 'Twenty One Pilots', name: 'Blurryface', sales: 738000 },
    { artist: 'Prince', name: 'The Very Best of Prince', sales: 668000 },
    { artist: 'Rihanna', name: 'Anti', sales: 603000 },
    { artist: 'Justin Bieber', name: 'Purpose', sales: 554000 }
];

const albumSalesStrings = musicData.map(function(music) {
    return `${music.name} by ${music.artist} sold ${music.sales} copies`;
});

console.log(albumSalesStrings);

/**
[ '25 by Adele sold 1731000 copies',
  'Views by Drake sold 1608000 copies',
  'Lemonade by Beyonce sold 1554000 copies',
  'Traveller by Chris Stapleton sold 1085000 copies',
  'A Pentatonix Christmas by Pentatonix sold 904000 copies',
  'Hamilton: An American Musical by Original Broadway Cast Recording sold 820000 copies',
  'Blurryface by Twenty One Pilots sold 738000 copies',
  'The Very Best of Prince by Prince sold 668000 copies',
  'Anti by Rihanna sold 603000 copies',
  'Purpose by Justin Bieber sold 554000 copies' ]
*/

filter()

数组的 filter() 方法与 map() 方法类似:

  • 它在一个数组上被调用
  • 它将一个函数作为参数
  • 它会返回一个新的数组

区别在于,传递给 filter() 的函数会被用作一个测试,只有数组中通过测试的项目会被包含在新的数组中。

const names = ['David', 'Richard', 'Veronika'];

const shortNames = names.filter(function(name) {
  return name.length < 6;
});
// ['David']

map() 一样,传递给 filter() 的函数会为 names 数组中的每个项目被调用。第一个项目(即 ‘David’)会被存储在 name 变量中。然后执行测试——这一步会进行过滤。首先,它会检查该名称的长度。如果它是 6 或更大,则会被跳过(而不会被包含在新数组中!)。相反,如果该名称的长度小于 6,那么 name.length < 6 则会返回 true,并且该名称会被包含在新数组中

因此,shortNames 将是新的数组 ['David']。请注意,它现在只包含一个名称,因为 'Richard''Veronika' 都有 6 个或更长的字符,因此都被过滤掉了。

const musicData = [
    { artist: 'Adele', name: '25', sales: 1731000 },
    { artist: 'Drake', name: 'Views', sales: 1608000 },
    { artist: 'Beyonce', name: 'Lemonade', sales: 1554000 },
    { artist: 'Chris Stapleton', name: 'Traveller', sales: 1085000 },
    { artist: 'Pentatonix', name: 'A Pentatonix Christmas', sales: 904000 },
    { artist: 'Original Broadway Cast Recording', 
      name: 'Hamilton: An American Musical', sales: 820000 },
    { artist: 'Twenty One Pilots', name: 'Blurryface', sales: 738000 },
    { artist: 'Prince', name: 'The Very Best of Prince', sales: 668000 },
    { artist: 'Rihanna', name: 'Anti', sales: 603000 },
    { artist: 'Justin Bieber', name: 'Purpose', sales: 554000 }
];

const results = musicData.filter(function(music) {
    const nameLength = music.name.length;
    return (nameLength >= 10) && (nameLength <= 25);
});

console.log(results);
/**
[ { artist: 'Pentatonix',
    name: 'A Pentatonix Christmas',
    sales: 904000 },
  { artist: 'Twenty One Pilots',
    name: 'Blurryface',
    sales: 738000 },
  { artist: 'Prince',
    name: 'The Very Best of Prince',
    sales: 668000 } ]
*/

数组方法

作用域

函数的作用域描述了给定函数内的可用变量。函数内的代码究竟能够访问什么呢?

  • 该函数的参数
  • 该函数内声明的局部变量
  • 来自其父函数作用域的变量
  • 全局变量

在这里插入图片描述

嵌套的 child() 函数可以访问所有 ab、和 c 变量,也就是说,这些变量都在 child() 函数的作用域内。

const myName = 'Andrew';
// 全局变量

function introduceMyself() {

  const you = 'student';
  // 已声明的变量,其中定义了 introduce()
  // (换句话说,在 introduce() 的父函数 introduceMyself() 中声明的变量)

  function introduce() {
    console.log(`Hello, ${you}, I'm ${myName}!`);
  }

  return introduce();
}

introduceMyself();
// Hello, student, I'm Andrew!

JavaScript 使用函数作用域

JavaScript 中的变量传统上是在函数作用域内定义的,而不是在块作用域内。由于输入一个函数会改变作用域,因此在函数内部定义的变量在该函数外部是不可用的。相反,如果在块中定义了任何变量(例如,在 if 语句中),则这些变量在该块外部是可用的

ES6 语法允许额外的作用域,并使用 letconst 关键字来声明变量。这些关键字在 JavaScript 中用于声明块作用域变量,并在很大程度上取代了使用 var 的需求。

作用域链

在这里插入图片描述

当解析变量时,JavaScript 引擎惠先查看嵌套子函数的局部定义变量。如果能够找到,则检索该值;否则,JavaScript 引擎会继续向外查找,直到变量被解析。如果 JavaScript 引擎已到达全局作用域,但仍然无法解析变量,则该变量为未定义。

变量阴影

当你所创建的变量与作用域链中的另一个变量具有相同名称时,会发生什么?

局部作用域的变量只会暂时“遮蔽”外部作用域中的变量。这被称为变量阴影。

const symbol = '¥';

function displayPrice(price) {
  const symbol = '$';
  console.log(symbol + price);
}

displayPrice('80');
// $80

总而言之,如果在不同上下文中的变量之间有任何命名重叠,则会通过从内部作用域到外部作用域(即从局部一直到全局)遍历作用域链来解决。因此,局部变量总是优先于更宽作用域内与其同名的变量

JavaScript 引擎会先查看最内层,然后向外查找——从直接在函数中定义的局部变量,直到全局作用域内的变量(如有必要)。

函数和函数作用域

闭包(Closure)

示例一

function outerFunction() {
  let num1 = 5;

  return function(num2) {
    console.log(num1 + num2);
  };
}

let result = outerFunction();

result(10);
// ???

outerFunction() 被返回后,看起来好像它的所有局部变量都会被分配回可用的内存。但是事实证明,嵌套的 innerFunction() 仍然可以访问 num1 变量!

outerFunction() 会返回一个对内部嵌套函数的引用。这个调用的返回值将保存在 result 中。当这个函数被调用时,它会保持对其作用域的访问;也就是它最初被定义的时候能够访问的所有变量。这包括在其父作用域中的 num1 变量。这个嵌套函数会遮蔽这些变量,只要对该函数本身的引用仍然存在,这些变量就会一直存在。

这样,当 result(10); 被执行时,该函数仍然可以访问 num1 的值 5。因此,15 会被记录到控制台。

示例二

function myCounter() {
	let count = 0;
	return function() {
		count += 1;
		return count;
	}
}
let counter = myCounter();
counter(); // 1
counter(); // 2
counter(); // 3
counter.count; // undefined
counte; // Uncaught ReferenceError: count is not defined...

使用闭包创建私有状态的真正好处是,闭包本身之外的任何函数,根本无法访问 count 状态或 count 数据。私有状态很实用,因为现在用户无法意外地重置该 count 了,外部函数根本无法访问该数据。

闭包的应用

闭包的两个常见和强大的应用:

  • 隐含地传递参数
  • 在函数声明中,存储作用域的快照

垃圾回收

JavaScript 通过自动垃圾回收来管理内存。这意味着,当数据不再可引用时(即没有可用于可执行代码的对该数据的剩余引用),它将被“垃圾回收”,并在稍后的某个时间点被销毁。这可以释放该数据曾经消耗的资源(即计算机内存),从而使这些资源可供重新使用。

父函数的变量可以被嵌套的内层函数访问。如果嵌套函数捕获并使用其父函数的变量(或其作用域链上的变量,如其父函数的父函数的变量),那么只要使用这些变量的函数仍可被引用,这些变量就会一直保留在内存中。

闭包是指函数和该函数声明位置的词法环境的组合。每次定义函数时,都会为该函数创建闭包。对于在一个函数中定义另一个函数的情况,闭包尤其强大,它让嵌套函数可以访问其外部的变量。即使父函数已返回,函数也会保留一个到其父作用域的链接。这可以防止父函数内的数据被垃圾回收

内存管理
闭包
词法环境(英)

立即调用函数表达式(IIFE)

函数声明与函数表达式

函数声明会定义一个函数,而不需要将变量赋给函数。它只是声明一个函数,而不会返回一个值

function returnHello() {
  return 'Hello!';
}

函数表达式会返回一个值。函数表达式可以是匿名或命名的,并且是另一个表达式语法的一部分。它们通常也会赋给变量

// 匿名
const myFunction = function () {
  return 'Hello!';
};

// 命名
const otherFunction = function returnHello() {
  return 'Hello!';
};

立即调用函数表达式:结构和语法

立即调用函数表达式或 IIFE 是在定义之后立即被调用的函数

(function sayHi(){
    alert('Hi there!');
  }
)();
// 展示 'Hi there!'

向 IIFE 传递参数

(function (name){
    alert('Hi, ' + name);
  }
)('Andrew');

// 展示 'Hi, Andrew'

(function (x, y){
    console.log(x * y);
  }
)(2, 3);

// 6

IIFE 和私有作用域

IIFE 的主要用途之一就是创建私有作用域。JavaScript 中的变量传统上遵循函数作用域。我们可以利用闭包的行为方式来保护变量或方法不被访问

const myFunction = (
  function () {
    const hi = 'Hi!';
    return function () {
      console.log(hi);
    }
  }
)();

在这里插入图片描述

myFunction 指向一个带有局部定义变量 hi 和一个返回函数的 IIFE,该函数会遮蔽 hi,并将其值输出到控制台。

  • IIFE 可以用于创建私有作用域
  • IIFE 与作用域和闭包密切相关
  • 有一种替代语法可以用于编写 IIFE(把第一个右括号移到最后)

IIFE、私有作用域和事件处理

假设我们想在页面上创建一个按钮,每隔一次点击就提醒用户。这样做的第一步思路可以是跟踪按钮被点击的次数。但是,我们应该如何保持这个数据呢?

我们可以使用在全局作用域内声明的一个变量来跟踪计数(如果应用程序的其他部分需要访问计数数据,这样做就很合理)。但是,更好的方式是将这些数据放在事件处理器中!首先,它可以防止我们使用额外的变量来污染全局(还可能会发生名称冲突)。更重要的是:如果我们使用 IIFE,我们就可以利用闭包来保护 count 变量不被外部访问!这可以防止意外的改变或未预期的连带结果无意中改变计数。

<!-- button.html -->
<html>
  <body>
     <button id='button'>Click me!</button>
     <script src='button.js'></script>
  </body>
</html>
// button.js
const button = document.getElementById('button');
button.addEventListener('click', (function() {
  let count = 0;

  return function() {
    count += 1;

    if (count === 2) {
      alert('This alert appears every other press!');
      count = 0;
    }
  };
})());

首先,我们声明了一个局部变量 count,它最初被设置为 0。然后,我们从_该_函数返回一个函数。所返回的函数会递增 count,但当计数达到 2 时,则会提醒用户,并将计数重置为 0。

需要特别指出的是,所返回的函数会遮蔽 count 变量。也就是说,由于函数会维持对其父函数作用域的引用,count 可供所返回的函数使用!因此,我们立即调用返回该函数的函数。而且,由于所返回的函数可以访问内部变量 count,所以创建了一个私有作用域,以便有效地保护该数据!我们将 count 放在一个闭包中,从而让我们可以保留每次点击的数据。

(function(n){
  delete n;
  return n;
})(2);// 返回值为2

delete 运算符实际上只会影响对象的属性;它不会用于直接释放资源(即释放内存),也不会影响变量或函数名称。因此,传入这个立即调用函数表达式的数字 2 将被返回。

立即调用函数表达式的好处

我们已经知道,使用立即调用函数表达式可以创建一个私有作用域来保护变量或方法不被访问。IIFE 最终会使用所返回的函数来访问闭包内的私有数据。这样做很有好处:虽然这些所返回的函数可以公开访问,但它们仍可保持内部定义变量的私有性

总而言之,如果你只想完成某个一次性任务(例如初始化应用程序),那么 IIFE 将是完成任务,同时避免额外变量污染全局环境的好办法。毕竟,清理全局名称空间可以减少重复变量名称冲突的几率。

JavaScript 设计模式(豆瓣)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值