目录
- [1. 定义函数](#1. 定义函数)
-
- [1.1 函数声明](#1.1 函数声明)
- [1.2 函数表达式](#1.2 函数表达式)
- [2. 调用函数](#2. 调用函数)
-
- [2.1 函数提升](#2.1 函数提升)
- [3. 函数作用域](#3. 函数作用域)
- [4. 作用域和函数栈](#4. 作用域和函数栈)
-
- [4.1 递归](#4.1 递归)
- [4.2 嵌套函数和闭包](#4.2 嵌套函数和闭包)
- [4.3 保存变量](#4.3 保存变量)
- [4.4 多层嵌套函数](#4.4 多层嵌套函数)
- [4.5 命名冲突](#4.5 命名冲突)
- [5. 闭包](#5. 闭包)
- [6. 使用arguments对象](#6. 使用arguments对象)
- [7. 函数参数](#7. 函数参数)
-
- [7.1 默认参数](#7.1 默认参数)
- [7.2 剩余参数](#7.2 剩余参数)
- [8. 箭头函数](#8. 箭头函数)
-
- [8.1 更简洁的函数](#8.1 更简洁的函数)
- [8.2 无单独的this](#8.2 无单独的this)
- [9. 预定义函数](#9. 预定义函数)
- 参考
- 待了解
1. 定义函数
1.1 函数声明
一个函数声明由function关键字,并跟随以下部分组成:
函数名称。
函数参数列表,包围在括号中并由逗号分隔。
定义函数的JavaScript语句,用大括号括起来。
javascript
function square(number) {
reutrn number * number;
}
函数square接受一个名为number的参数。这个函数只有一个语句,其表示该函数将函数的参数自乘后返回。函数的return语句指定了函数的返回值。
参数是按值传递给函数的。即使函数体的代码为传递给函数的参数赋了新值,这个改变也不会反映到全局或调用该函数的代码中。
如果你将对象作为参数传递,而函数改变了这个对象的属性,这样的改变对函数外部是可见的。
javascript
function myFunc(theObject) {
theObject.make = "Toyota";
}
const mycar = {
make: "Honda",
model: "Accord",
year: 1998,
};
console.log(mycar.make);
myFunc(mycar);
console.log(mycar.make);

如果你将数组作为参数传递,而函数改变了这个数组的值,这样的改变对函数外部也同样可见。
javascript
function myFunc(theArr) {
theArr[0] = 30;
}
const arr = [45];
console.log(arr[0]);
myFunc(arr);
console.log(arr[0]);

1.2 函数表达式
虽然上面的函数声明在语法上是一个语句,但函数也可以由函数表达式创建。
这样的函数可以是匿名的;它不必有一个名称。
javascript
const square = function (number) {
return number * number;
};
console.log(square(4));

然而,也可以为函数表达式提供名称,并且可以用于在函数内部代指其本身,或者在调试器堆栈跟踪中识别该函数。
javascript
const factorial = function fac(n) {
return n < 2 ? 1 : n * fac(n - 1);
};
console.log(factorial(3));

当将函数作为参数传递给另一个函数时,函数表达式很方便。
javascript
function map(f, a) {
const result = new Array(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = f(a[i]);
}
return result;
}
const cube = function (x) {
return x * x * x;
};
const numbers = [0, 1, 2, 5, 10];
console.log(map(cube, numbers));

在JavaScript中,可以根据条件来定义一个函数。
javascript
let myFunc;
if (num == 0) {
myFunc = function (theObject) {
theObject.make = "Toyota";
};
}
当一个函数是一个对象的属性时,称之为方法。
2. 调用函数
定义的函数并不会自动执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做什么。
调用函数才会以给定的参数真正执行这些动作。函数一定要处于调用它们的作用域中,但是函数声明可以被提升(出现在调用语句之后)。函数声明的范围是声明它的函数之内。
函数本身也是对象,也有方法。
2.1 函数提升
javascript
console.log(square(5));
function square(n) {
return n * n;
}

尽管square函数在声明之前被调用,但此代码的运行没有错误。这是因为JavaScript计时器会将整个函数声明提升到当前作用域的顶部。
javascript
// 所有函数声明实际都位于作用域的顶部
function square(n) {
return n * n;
}
console.log(square(5));
函数提升仅使用于函数声明,而不适用于函数表达式。
javascript
console.log(square(5));
const square = function (n) {
return n * n;
};

3. 函数作用域
在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的作用域内定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。
换而言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其复函数中定义的所有变量和复函数有权访问的任何其他变量。
javascript
// 下面的变量定义在全局作用域中
const num1 = 20;
const num2 = 3;
const name = "Chamakh";
// 此函数定义在全局作用域中
function multiply() {
return num1 * num2;
}
console.log(multiply());
function getScore() {
const num1 = 2;
const num2 = 3;
function add() {
return `${name}的得分为${num1 + num2}`;
}
return add();
}
console.log(getScore());

4. 作用域和函数栈
4.1 递归
一个函数可以指向并调用自身。有三种方法可以达到这个目的:
1. 函数名
2. arguments.callee
3. 作用域内一个指向该函数的变量名
javascript
const foo = function bar() {
// 这里编写语句
};
在这个函数体,以下的语句是等价的:
1. bar()
2. arguments.callee()
3. foo()
这些调用自身的函数我们称之为递归函数。在某种意义上说,递归近似于循环。两者都重复执行相同的代码,并且两者都需要一个终止条件,避免无限循环或递归。
递归可以通过栈转换成循环。
4.2 嵌套函数和闭包
可以在一个函数里面嵌套另一个函数。嵌套函数对其容器函数时私有的。它自身也形成了一个闭包。闭包是可以拥有独立变量以及绑定了这些变量的环境的表达式。
既然嵌套函数是一个闭包,意味着一个嵌套函数可以继承容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。
javascript
function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
const fnInside = outside(3);
console.log(fnInside(5));
console.log(outside(3)(5));

4.3 保存变量
上面例子中inside被返回时x是怎么被保留下来的。一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的inside没有再被引用时,内存才会释放。
4.4 多层嵌套函数
函数可以被多层嵌套。因此闭包可以包含多个作用域;它们递归地包含了所有包含它的作用域。这个称之为作用域链。
javascript
function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z);
}
C(3);
}
B(2);
}
A(1);

在这个例子中,C可以访问B的y和A的x。
B形成了一个包含A的闭包;C形成了一个包含B的闭包;C的闭包包含B,且B的闭包包含A,所以C的闭包也包含A。C可以同时访问B和A的参数和变量。换言之,C用这个顺序链接了B和A的作用域。
4.5 命名冲突
当同一个闭包作用域下两个参数或者变量同名时,就会产生命名冲突。更近的作用域有更高的优先级,所以最近的优先级最高,最远的优先级最低。这就是作用域链。链的第一个元素就是最里面的作用域,最后一个元素便是最外层的作用域。
javascript
function outside() {
const x = 5;
function inside(x) {
return x * 2;
}
return inside;
}
console.log(outside()(10));

命名冲突发生在语句return x * 2;上,inside的参数x和outside的变量x发生冲突。这里的作用域链是(inside、outside、全局对象)。因此inside的x优先于outside的x,因此返回20而不是10。
5. 闭包
闭包是JavaScript中最强大的特性之一。JavaScript允许函数嵌套,并且内部函数具有定义在外部函数中的所有变量和函数以及外部函数能访问的所有变量和函数的完全访问权限。
但是外部函数不能访问定义在内部函数中的变量和函数。这个内部函数的变量提供了一种封装。
此外,由于内部函数可以访问外部函数的作用域,因此当内部函数生存周期大于外部函数时,外部函数定义的变量和函数的生存周期将比内部函数执行的持续时间要长。当内部函数以某一种方式被任何一个外部函数之外的任何作用域访问时,就会创建闭包。
javascript
// 外部函数定义了一个名为"name"的变量
const pet = function (name) {
const getName = function() {
// 内部函数可以访问外部函数的"name"变量
return name;
};
return getName; // 返回内部函数,从而将其暴露给外部作用域
};
const myPet = pet("Vivie");
console.log(myPet());

实际上可能会比上面的代码复杂的多。它可以返回一个包含用于操作外部函数的内部变量的方法的对象。
javascript
const createPet = function (name) {
let sex;
const pet = {
// 在这个上下文中:setName(newName)等价于setName:function(newName)
setName(newName) {
name = newName;
},
getName() {
return name;
},
getSex() {
return sex;
},
setSex(newSex) {
if (
typeof newSex === "string" &&
(newSex.toLowerCase() === "male" || newSex.toLowerCase() === "female")) {
sex = newSex;
}
},
};
return pet;
};
const pet = createPet("Vivie");
console.log(pet.getName());
pet.setName("Oliver");
pet.setSex("male");
console.log(pet.getSex());
console.log(pet.getName());

6. 使用arguments对象
函数的实际参数会被保存在一个类似数组的arguments对象中。在函数内,你可以按如下方式找出传入的参数。
javascript
arguments[i]
其中i是参数的序号,从0开始。所以第一个传入函数的参数会是arguments0。参数的数量由arguments.length表示。
使用arguments对象,你可以处理比声明更多的参数来调用函数。这在你事先不知道会需要将多少参数传递给函数时十分有用。
javascript
function myConcat(separator) {
let result = "";
for (let i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}
console.log(myConcat("、", "红", "橙", "蓝"));
console.log(myConcat(":", "大象", "长颈鹿", "狮子", "猎豹"));
console.log(myConcat("、", "智者", "罗勒", "牛至", "胡椒", "香菜"));

7. 函数参数
有两种特殊的参数语法:默认参数和剩余参数。
7.1 默认参数
在JavaScript中,函数参数的默认值是undefined。然而,在某些情况下设置不同的默认值可能会很有用。这正是默认参数的作用。
在过去,用于设定默认参数的一般策略是在函数的主体中测试参数值是否为undefined,如果是则赋予这个参数一个默认值。
在下面的示例中,如果调用函数时没有给b提供值,那么它的值就是undefined,在执行a * b时,调用乘法通常会返回NaN。但是,这已经被示例的第二行所避免了:
javascript
function multiply(a, b) {
b = typeof b !== "undefined" ? b : 1;
return a * b;
}
console.log(multiply(5));

使用默认参数,在函数体的手动检查就不再必要了。现在,可以在函数头简单地把1设定为b的默认值:
javascript
function multiply(a, b = 1) {
return a * b;
}
console.log(multiply(5));

7.2 剩余参数
剩余参数语法允许将不确定数量的参数表示为数组。
在下面的示例中,multiply函数使用剩余参数收集从第二个参数开始到最后的参数。然后,该函数将它们与第一个参数相乘。
javascript
function multiply(multiplier, ...theArgs) {
return theArgs.map((x) => multiplier * x);
}
const arr = multiply(2, 1, 2, 3);
console.log(arr);

8. 箭头函数
箭头函数表达式相比函数表达式具有较短的语法且没有它自己的this、arguments、super和new.target。箭头函数总是匿名的。
有两个因素会影响箭头函数的引入:更简洁的函数和this的无绑定性。
8.1 更简洁的函数
在一些函数模式中,更简洁的函数很受欢迎。
javascript
const a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];
const a2 = a.map(function (s) {
return s.length;
});
console.log(a2);
const a3 = a.map((s) => s.length);
console.log(a3);

8.2 无单独的this
在箭头函数出现之前,每一个新函数都定义了自己的this值(在构造函数中是一个新的对象;在严格模式下是undefined;在作为对象方法调用的函数中指向这个对象)。事实证明,这对于面向对象的编程风格来说并不理想。
javascript
function Person() {
// 构造函数 Person() 将 this 定义为自身
this.age = 0;
[1, 2, 3].forEach(function growUp() {
// 非严格模式下,forEach 以默认绑定调用回调,this === window
this.age++;
});
}
var age = 0; // 用 var:挂在 window 上,让 this.age 够得着
const p = new Person();
console.log(p.age, age); // 0 3 ------ 实例纹丝不动,全局 age 却加了 3 次!

在ECMAScript 3/5中,通过把this的值赋值给一个变量可以修复这个问题。
javascript
function Person() {
// 有的人习惯用 `that` 而不是 `self`。
// 请选择一种方式,并保持前后代码的一致性
const self = this;
self.age = 0;
[1, 2, 3].forEach(function growUp() {
// 回调引用 `self` 变量,其值为预期的对象。
self.age++;
});
}
const p = new Person();
console.log(p.age);

另外,创建一个绑定函数可以使得this值被正确传递给growUp()函数。
箭头函数没有自己的this,而是使用封闭执行上下文的this值。
javascript
function Person() {
this.age = 0;
[1, 2, 3].forEach(() => this.age++); // 箭头函数没有自己的 this,捕获外层的实例
}
const p = new Person();
console.log(p.age); // 3 ------ 正确

9. 预定义函数
JavaScript语言有几个顶级的内置函数:
eval():eval()方法会以字符串表示的JavaScript代码进行求值。
isFinite():isFinite()函数用于判断传入的值是否为有限数。如果需要的话,其参数首先被转换成一个数值。
isNaN():isNaN()函数判断一个值是否是NaN。isNaN函数内部的强制转换规则十分有趣。也可以使用Number.isNaN()来判断该值是否为NaN。
parseFloat():parseFloat()函数解析字符串参数,并返回一个浮点数。
parseInt():parseInt()函数解析字符串参数,并返回指定的基数的整数。
decodeURI():decodeURI()函数对先前经过encodeURI函数或者其他类似方法编码过的统一资源标志符URI进行解码。
decodeURIComponent():decodeURIComponent()方法对先前经过encodeURIComponent函数或者其他类似方法编码的统一资源标识符URI进行解码。
encodeURI():encodeURI()方法通过以表示字符的UTF-8编码的一个、两个、三个或四个转义序列替换统一资源标识符URI的某些字符来进行编码(对于由两个代理surrogate字符组成的字符,只会编码为四个转义序列)。
encodeURIComponent():encodeURIComponent()方法通过以表示字符的UTF-8编码的一个、两个、三个或四个转义序列替换统一资源标识符URI的某些字符来进行编码(对于由两个代理surrogate字符组成的字符,只会编码为四个转义序列)。
escape():escape()方法生成一个新的字符串,其中的某些字符已被替换为十六进制转义序列。已弃用,使用encodeURI()或encodeURIComponent()。
unescape():unescape()方法计算生成一个新的字符串,其中的十六进制转义序列将被其表示的字符替换。已弃用,使用decodeURI()或decodeURIComponent()。
参考
1、函数
待了解
1、Function