JS理解函数
2023-10-10 16:21:03 #JS

函数是第一类对象

  • 函数也是对象
  • JavaScript 中函数拥有对象的所有能力,对象能做的任何一件事,函数也都能做
  • 函数的特殊之处在于,它可以被调用,以便执行某项动作
  • JS 使用函数式编程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过字面量创建
function myFunction() {}

// 赋值给变量、数组项或其他对象的属性
let myFunction = function () {}; // 为变量赋值一个新函数
myArray.push(function () {}); // 向数组中增加一个新函数
zheng.data = function () {}; // 给某个对象的属性赋值为一个新函数

// 作为函数的参数来传递
function call(myFunction) {
myFunction();
}
call(function () {}); // 一个新函数作为参数传递给函数

// 作为函数的返回值
function myFunction() {
return function () {}; // 返回一个新函数
}

// 具有动态创建和分配的属性
let myFunction = function () {};
myFunction.myname = 'Bobo'; // 为函数增加一个新属性

回调函数

  • 回调函数,指的是,在程序执行过程中,建立的函数会被其他函数在稍后的某个合适时间点“再回来调用”
  • 函数 A 作为参数(函数引用)传递到另一个函数 B 中,并且这个函数 B 执行函数A。那么函数 A 叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数
1
2
3
4
5
6
7
8
// 函数 A 作为回调函数
function B(a) {
return a();
}
function A() {
console.log('i am A');
}
B(A); // i am A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 一个简单的回调函数例子
const text = 'Demo callback!';
console.log('before defining functions');
function useless(callback) {
console.log('in useless function');
return callback();
}
function getText() {
console.log('in getText function');
return text;
}
console.log('before making all the calls');
// 调用 useless(getText) 会触发 useless 函数的执行,随后会触发 getText 函数的执行,即对 getText 函数进行回调
if (useless(getText) === text) {
console.log('the useless function works! ' + text);
}
console.log('after the calls have been made');

// 打印顺序
// before defining functions
// before making all the calls
// in useless function
// in getText function
// the useless function works! Demo callback!
// after the calls have been made
1
2
3
4
5
// 浏览器也可以调用回调函数
const button = document.getElementById('button');
button.addEventListener('click', function () {
alert('ha!');
})
1
2
3
4
5
// 使用 sort() 降序排序(sort() 方法默认升序排序)
let values = [8, 5, 6, 0, 3, 2, 9];
values.sort(function (value1, value2) {
return value2 - value1;
})

函数的属性

  • 函数具有属性,而且这些属性可以被存储任何信息
  • 给函数添加一个新属性后,可以使用该属性修改函数本身

使用场景1:在函数属性中存储另一个函数用于之后的引用和调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 存储唯一函数集合
let store = {
nextId: 1, // 跟踪下一个要被赋值的 id
cache: {}, // 使用一个对象作为缓存,可以在其中存储函数
add: function(fn) { // 仅当函数唯一时,将该函数加入缓存
if (!fn.id) { // 给函数添加属性 id
fn.id = this.nextId++;
this.cache[fn.id] = fn;
return true;
}
}
}

function chen() {}
function zheng() {}
store.add(chen);
store.add(zheng);
store.add(zheng);

console.log('store:', store);
// store.cache: chen(), zheng()
// store.nextID: 3

使用场景2:用函数属性创建一个缓存(记忆),用于减少不必要的计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 一个大于 1 的自然数,除了 1 和它自身外,不能被其他自然数整除的数叫做质数
* @param value 要判断的数是否为质数
*/
function isPrime(value) {
// 初次调用函数时,创建函数属性为初始空对象,作为缓存
if (!isPrime.caches) {
isPrime.caches = {};
}
// 检查参数中传的值是否已经存储到缓存中,如果在缓存中找到该值,函数直接返回
if (isPrime.caches[value] !== undefined) {
console.log(value + '已缓存');
return isPrime.caches[value];
}
// 若 value 是质数,则 prime 为 true,否则 prime 为 false
let prime = value !== 0 && value !== 1; // 0、1 不是质数
for (let i = 2; i < value; i++) {
if (value % i === 0) {
prime = false;
break;
}
}
// 存储计算的值
return isPrime.caches[value] = prime;
}

console.log(isPrime(0) ? '0是质数' : '0不是质数'); // 0不是质数
console.log(isPrime(1) ? '1是质数' : '1不是质数'); // 1不是质数
console.log(isPrime(2) ? '2是质数' : '2不是质数'); // 2是质数
console.log(isPrime(3) ? '3是质数' : '3不是质数'); // 3是质数
console.log(isPrime(4) ? '4是质数' : '4不是质数'); // 4不是质数
console.log(isPrime(9) ? '9是质数' : '9不是质数'); // 9不是质数
console.log(isPrime(4) ? '4是质数' : '4不是质数'); // 4已缓存 4不是质数

函数定义

函数声明

  • 函数声明必须独立,但也能够被包含在其他函数或代码块中
  • 必须有函数名,以便被调用
1
2
3
4
5
6
7
8
9
10
11
12
function zheng() {
return 'zheng here';
}
zheng(); // 'zheng here'

function bobo () {
function hiddenBobo () {
return 'bobo here';
}
return hiddenBobo();
}
bobo(); // 'bobo here'

函数表达式

  • 通常作为其他语句中的一部分
  • 可以赋值给变量和属性,可以作为传递给其他函数的参数或函数的返回值
  • 函数名可选,被调用时可以是赋值给的变量,也可以是函数中的参数等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let myFunc = function () {      // 函数表达式作为变量声明赋值语句中的一部分
};

myFunc(function () { // 函数表达式作为一次函数调用中的参数
return function () { // 函数表达式作为函数返回值
};
});

// 括号 () 和一元操作符 + - ! ~ 都可以表示立即调用函数表达式
(function namedFunctionExpression() { // 立即调用命名函数表达式
})();

(function () { // 立即调用匿名函数表达式
})();

+function () {
}();

-function () {
}();

!function () {
}();

~function () {
}();

箭头函数

  • (param1, param2) => expression
  • 箭头函数是函数表达式的简化版
  • 只有一个参数时,括号不是必须的
  • 函数可以是一个表达式(省略 return 关键字,返回值即表达式的值),也可以是一个代码块(返回值与普通函数一样:如果没有 return 语句,返回值是 undefined;反之,返回值就是 return 表达式的值)
1
2
3
4
// 将数组降序排序
const values = [0, 3, 4, 6, 9, 1];
values.sort((v1, v2) => v2 - v1);
// values = [9, 6, 4, 3, 1, 0]

函数的实参和形参

  • 形参是定义函数时所列举的变量
  • 实参是调用函数时所传递给函数的值
  • 当函数调用时,实参会按形参在函数中定义的顺序赋值给形参
  • 如果实参的数量大于形参,那么额外的实参不会被赋值给任何形参
  • 如果形参的数量大于实参,那么额外的形参会被设为 undefined

剩余参数

  • 以…作为前缀的命名参数,放在函数参数的最后一个(否则报错),表示一个不定数量的数组
1
2
3
4
5
6
// 第一个参数与剩余参数的最大值相乘
function multiMax(first, ...remainingNumbers) {
let sorted = remainingNumbers.sort((a, b) => b - a); // 降序排序
return first * sorted[0];
}
multiMax(3, 1, 3, 5); // 15

默认参数

  • ES6 中,创建默认参数的方式是,为函数的形参赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
// ES6 之前处理默认参数的方式
function performAction(name, action) {
action = typeof action === 'undefined' ? 'walking' : action;
return name + ' ' + action;
}

// ES6 处理默认参数的方式
function performAction(name, action = 'walking') {
return name + ' ' + action;
}

performAction('zheng'); // zheng walking
performAction('zheng', 'running') // zheng running
  • 可以为默认参数赋任何值,既可以是数字或者字符串这样的基本类型,也可以是对象、数组,甚至函数这样的引用类型
  • 每次函数调用都会从左到右求得参数的值,并且当对后面的默认参数赋值时可以引用前面的默认参数(可读性差)

使用隐式函数参数

arguments 参数

  • 在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数
  • arguments 对象是类数组(并不是Array的实例),使用 length 属性来确定传进来多少个参数(不是定义函数时的命名参数的个数)
  • arguments 对象可以和命名参数一起使用,arguments 的值永远与对应的命名参数保持同步(只是值同步,内存空间是独立的)
  • 将 arguments 对象作为函数参数的别名使用时会影响代码的可读性,因此在严格模式("use strict")下无法使用它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function whatever(a, b, c) {
// 值的准确性校验
console.log(a === 1); // true
console.log(b === 2); // true
console.log(c === 3); // true

// 共传入的参数个数
console.log(arguments.length); // 5

// 验证传入的前3个实参与函数的3个形参匹配
console.log(arguments[0] === a); // true
console.log(arguments[1] === b); // true
console.log(arguments[2] === c); // true

// 验证额外的参数可以通过参数 arguments 获取
console.log(arguments[3] === 4); // true
console.log(arguments[4] === 5); // true
}

whatever(1, 2, 3, 4, 5);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用 arguments 参数对所有函数参数进行求和
function sum() {
let sum = 0;
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}

console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3)); // 6
console.log(sum(3, 5, 6, 7)); // 21

// 使用剩余参数改写上例
function sum2(...numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}

console.log(sum2(5, 6, 7, 8, 9)); // 35
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// arguments 对象作为函数参数的别名
function func(person) {
console.log(person === 'chen'); // true
console.log(arguments[0] === 'chen'); // true

// 改变 arguments 对象的值也会改变相应的形参
arguments[0] = 'zheng';
console.log(person === 'zheng'); // true
console.log(arguments[0] === 'zheng'); // true

person = 'liu';
console.log(person === 'liu'); // true
console.log(arguments[0] === 'liu'); // true
}

func('chen');

this 参数

  • 调用函数时,this 参数也会默认地传递给函数
  • this 参数代表函数调用相关联的对象,称之为:函数上下文
  • this 参数的指向不仅由定义函数的方式和位置决定,还受到函数调用方式的影响

函数调用方式

  • 作为一个函数 getUserInfo() 直接被调用
  • 作为一个方法 userInfo.getName() 关联在一个对象上,实现面向对象编程
  • 作为一个构造函数 new UserInfo() 实例化一个新的对象
  • 通过函数的 apply 方法 getUserInfo.apply(userInfo) 或 call 方法 getUserInfo.call(userInfo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 作为函数调用
function getUserInfo(name) {
}
getUserInfo('Chen');

(function (who) {
return who;
})('Chen');

// 作为对象的一个方法调用
let zhao = {
getName: function () {
}
}
zhao.getName();

// 作为构造函数调用
function Zhao(name) {
}
zhao = new Zhao();

// 通过 call 方法调用
getUserInfo.call(zhao, 'Wong');

// 通过 apply 方法调用
getUserInfo.apply(zhao, ['Wong']);

this 指向

作为函数直接被调用

  • 非严格模式下,this 指向全局 window 对象
1
2
3
4
function getThis() {
return this;
}
getThis(); // window 对象
  • 严格模式下,this 指向 undefined
1
2
3
4
5
function getThis() {
'use strict';
return this;
}
getThis(); // undefined

作为方法被调用

  • 当一个函数被赋值给一个对象的属性,并且通过对象属性引用的方式调用函数时,函数会作为对象的方法被调用
  • 当函数作为某个对象的方法被调用时,this 指向该对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 直接通过函数名,调用函数
function whatsMyContext() {
return this;
}
console.log(whatsMyContext()); // window 对象

// 通过变量创建函数引用,调用函数
let getMyThis = whatsMyContext;
console.log(getMyThis()); // window 对象

// 通过对象的属性创建函数引用,即,函数作为对象方法被调用
let zhou = {
getMyThis: whatsMyContext
}
console.log(zhou.getMyThis() === zhou); // true

let wu = {
getMyThis: whatsMyContext
}
console.log(wu.getMyThis() === wu); // true

作为构造函数被调用

  • 函数作为构造函数被调用时,this 指向新创建的对象实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 使用构造函数来实现通用对象
    function Cat() {
    this.color = function () {
    return this;
    }
    }

    let cat1 = new Cat();
    let cat2 = new Cat();

    console.log(cat1.color() === cat1); // true
    console.log(cat2.color() === cat2); // true
  • 使用 new 调用构造函数会经历以下4个步骤

    • 创建一个新的空对象
    • 将构造函数的 this 指向新对象
    • 执行构造函数中的代码(为这个新对象添加属性)
    • 返回新对象
  • 构造函数的目的

    • 创建一个新对象,并进行初始化
    • 然后将其作为构造函数的返回值
  • 构造函数的返回值

    • 如果构造函数返回的是一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的 this 将被丢弃
    • 如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
function Dog() {
this.color = function () {
return this;
}
return 1; // 构造函数返回一个确定的基本类型值
}

console.log(Dog() === 1); // true 直接以函数形式被调用,返回值为1

let dog1 = new Dog();

console.log(typeof dog1 === 'object'); // true 以构造函数形式被调用,返回值1被忽略了
console.log(typeof dog1.color === 'function'); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let puppet = {
rules: false
}

// 返回一个全局对象的构造函数
function Emperor() {
this.rules = true;
return puppet;
}

let emperor = new Emperor();

console.log(emperor === puppet); // true
console.log(emperor.rules); // false

// 返回一个非对象的构造函数,忽略该返回值
function Emperor2() {
this.rules = true;
return false;
}

let emperor2 = new Emperor2();

console.log(emperor2 === false); // false
console.log(emperor2.rules); // true

使用 apply 和 call 方法调用

  • 每个函数都可以使用 call 或 apply 方法来显式地指定任何对象作为函数上下文
  • 通过 apply 或 call 方法调用函数,this 指向 apply 或 call 方法的第一个参数
  • apply 方法参数:作为函数上下文(this)的对象,作为函数实参的一个数组
  • call 方法参数:作为函数上下文(this)的对象,作为函数实参的一个或多个列表
  • 常用于调用回调函数时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 apply 和 call 方法来设置函数上下文
function juggle() {
let result = 0;
for(let i = 0; i < arguments.length; i++) {
result += arguments[i];
}
this.result = result;
}

let juggle1 = {};
let juggle2 = {};

juggle.apply(juggle1, [1, 2, 3, 4]);
juggle.call(juggle2, 5, 6, 7, 8);

console.log(juggle1.result); // 10
console.log(juggle2.result); // 26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 强制指定回调函数的 this 值,实现数组 forEach 方法
function forEach(list, callback) {
for (let i = 0; i < list.length; i++) {
callback.call(list[i], i);
}
}

let animals = [
{type: 'cats'},
{type: 'dogs'},
{type: 'birds'}
];

forEach(animals, function (index) {
console.log(animals[index].type);
console.log(this === animals[index]); // true
})

使用箭头函数

  • 箭头函数没有单独的 this 值,this 在箭头函数创建时确定
  • 调用箭头函数时,不会隐式传入 this 参数,而是从定义时的所在函数继承上下文
  • 如果,箭头函数在构造函数内部,this 指向新创建的对象本身
1
2
3
4
5
6
7
8
9
10
11
12
// 箭头函数作为构造函数的方法
function Button() {
this.clicked = false;
this.click = () => {
this.clicked = true;
console.log(button.clicked); // true
}
}

let button = new Button();
let elem = document.getElementById('test');
elem.addEventListener('click', button.click);
  • 如果,箭头函数是作为对象字面量的属性在全局中定义的,那么箭头函数内部的 this 值等于全局代码的 this 值,即 window 对象(严格模式下是 undefined)
1
2
3
4
5
6
7
8
9
10
11
12
// 箭头函数作为对象字面量的属性
let button2 = {
clicked: false,
click: () => {
this.clicked = true;
console.log(button2.clicked); // false
console.log(this === window); // true
console.log(window.clicked); // true
}
}
let elem2 = document.getElementById('test2');
elem2.addEventListener('click', button2.click);

使用 bind 方法

  • 所有函数均可访问 bind 方法,可以创建并返回一个新函数,并绑定到传入的对象上
  • 无论如何调用该函数,this 均被设置为对象本身
  • bind 方法创建的新函数与原始函数的函数体一致,行为一致
  • 调用 bind 方法不会修改原始函数,而是创建了一个全新的函数
1
2
3
4
5
6
7
8
9
10
// bind 方法在事件处理中绑定指定上下文
let button3 = {
clicked: false,
click: function () {
this.clicked = true;
console.log(button3.clicked); // true
}
}
let elem3 = document.getElementById('test3');
elem3.addEventListener('click', button3.click.bind(button3));