函数
每个函数都是Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是 指向函数对象的指针,而且不一定与函数本身紧密绑定。
// 声明函数
function sum (num1, num2) {
return num1 + num2;
}
// 函数表达式
let sum = function(num1, num2) {
return num1 + num2;
};
// 箭头函数
let sum = (num1, num2) => {
return num1 + num2;
};
// 构造函数-不推荐
// 这段代码会被解释两次:第一次是将它当作常规ECMAScript 代码,第二次是解释传给构造函数的字符串。这显然会影响性能
let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
箭头函数
箭头函数实例 化的函数对象与正式的函数表达式创建的函数对象行为是相同的。
let arrowSum = (a, b) => {
return a + b;
};
let functionExpressionSum = function(a, b) {
return a + b;
};
console.log(arrowSum(5, 8)); // 13
console.log(functionExpressionSum(5, 8)); // 13
箭头函数简洁的语法非常适合嵌入函数的场景:
let ints = [1, 2, 3];
console.log(ints.map(function(i) { return i + 1; })); // [2, 3, 4]
console.log(ints.map((i) => { return i + 1 })); // [2, 3, 4]
只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号
attention: 无效写法
let multiply = a, b => { return a * b; };
箭头函数也可以不用大括号,但这样会改变函数的行为。不使用大括号,那么箭头后面就只能有一行代码, 比如一个赋值操作,或者一个表达式。而且,省略大括号会隐式返回这行代码的值.
// 以下两种写法都有效,而且返回相应的值
let double = (x) => { return 2 * x; };
let triple = (x) => 3 * x;
// 无效的写法:
let multiply = (a, b) => return a * b;
箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。
函数名
函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着 一个函数可以有多个名称
function sum(num1, num2) {
return num1 + num2;
}
console.log(sum(10, 10)); // 20
let anotherSum = sum;
console.log(anotherSum(10, 10)); // 20
sum = null;
console.log(anotherSum(10, 10)); // 20
ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。如果它是使用 Function 构造函数创建的,则会标识成"anonymous":
function foo() {}
let bar = function() {};
let baz = () => {};
console.log(foo.name); // foo
console.log(bar.name); // bar
console.log(baz.name); // baz
console.log((() => {}).name); //(空字符串)
console.log((new Function()).name); // anonymous
如果函数是一个获取函数、设置函数,或者使用 bind()实例化,那么标识符前面会加上一个前缀:
function foo() {}
console.log(foo.bind(null).name); // bound foo
理解函数
ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的。
在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。
arguments 对象是一个类数组对象,第一个参数是 arguments[0],第二个参数是 arguments[1],要确定传进来多少个参数, 可以访问 arguments.length 属性。
function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
// arguments
function sayHi() {
console.log("Hello " + arguments[0] + ", " + arguments[1]);
}
arguments 对象可以跟命名参数一起使用,比如:
// 命名参数 num1 保存着与arugments[0]一样的值,因此使用谁都无所谓。
function doAdd(num1, num2) {
if (arguments.length === 1) {
console.log(num1 + 10);
} else if (arguments.length === 2) {
console.log(arguments[0] + num2);
}
}
arguments 对象的另一个有意思的地方就是,它的值始终会与对应的命名参数同步。
修改 arguments[1]也会修改 num2 的值,因此两者的值都是 10。但这并不意味着它们都 访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。
如果只 传了一个参数,然后把 arguments[1]设置为某个值,那么这个值并不会反映到第二个命名参数。这是 因为 arguments 对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。
function doAdd(num1, num2) {
arguments[1] = 10;
console.log(arguments[0] + num2);
}
箭头函数中的参数
如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只 能通过定义的命名参数访问。
function foo() {
console.log(arguments[0]);
}
foo(5); // 5
let bar = () => {
console.log(arguments[0]);
};
bar(5); // ReferenceError: arguments is not defined
虽然箭头函数中没有 arguments 对象,但可以在包装函数中把它提供给箭头函数:
function foo() {
let bar = () => {
console.log(arguments[0]); // 5
};
bar();
}
foo(5);
Attention:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XbwombLH-1684760697446)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230521152306974.png)]
没有重载
ECMAScript 函数不能像传统编程那样重载。比如 Java 中,一个函数可以有两个定义, 只要签名(接收参数的类型和数量)不同就行。
ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。
ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的:
function addSomeNumber(num) {
return num + 100;
}
function addSomeNumber(num) {
return num + 200;
}
let result = addSomeNumber(100); // 300
默认参数值
ECMAScript 6支持显式定义默认参数了。
function makeKing(name = 'Henry') {
return `King ${name} VIII`;
}
console.log(makeKing('Louis')); // 'King Louis VIII'
console.log(makeKing()); // 'King Henry VIII'
给参数传 undefined 相当于没有传值,不过这样可以利用多个独立的默认值:
function makeKing(name = 'Henry', numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
console.log(makeKing()); // 'King Henry VIII'
console.log(makeKing('Louis')); // 'King Louis VIII'
console.log(makeKing(undefined, 'VI')); // 'King Henry VI'
在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。
function makeKing(name = 'Henry') {
name = 'Louis';
return `King ${arguments[0]}`;
}
console.log(makeKing()); // 'King undefined'
console.log(makeKing('Louis')); // 'King Louis'
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值
let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI'];
let ordinality = 0;
function getNumerals() {
// 每次调用后递增
return romanNumerals[ordinality++];
}
function makeKing(name = 'Henry', numerals = getNumerals()) {
return `King ${name} ${numerals}`;
}
console.log(makeKing()); // 'King Henry I'
函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数 只有在调用函数但未传相应参数时才会被调用。
默认参数作用域与暂时性死区
给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样。参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛 出错误:
// 调用时不传第一个参数会报错
function makeKing(name = numerals, numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
参数也存在于自己的作用域中,它们不能引用函数体的作用域:
function makeKing(name = 'Henry', numerals = defaultNumeral) {
let defaultNumeral = 'VIII';
return `King ${name} ${numerals}`;
}
参数扩展与参数收集
ECMAScript 6 新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。...
扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。
扩展参数
如下函数定义,它会将所有传入的参数累加起来:
let values = [1, 2, 3, 4];
function getSum() {
let sum = 0;
for (let i = 0; i < arguments.length; ++i) {
sum += arguments[i];
}
return sum;
}
如果不使用扩 展操作符,想把定义在这个函数这面的数组拆分,那么就得求助于 apply()方法:
console.log(getSum.apply(null, values)); // 10
在 ECMAScript 6 中,可以通过扩展操作符极为简洁地实现这种操作:
console.log(getSum(...values)); // 10
console.log(getSum(-1, ...values)); // 9
console.log(getSum(...values, 5)); // 15
收集参数
在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。过收集参数的结果会得到一个 Array 实例:
function getSum(...values) {
// 顺序累加 values 中的所有值
// 初始值的总和为 0
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3)); // 6
收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集 参数的结果可变,所以只能把它作为最后一个参数:
// 不可以
function getProduct(...values, lastValue) {}
// 可以
function ignoreFirst(firstValue, ...values) {
console.log(values);
}
箭头函数虽然不支持 arguments 对象,但支持收集参数的定义方式,因此也可以实现与使用 arguments 一样的逻辑:
let getSum = (...values) => {
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3)); // 6
使用收集参数并不影响 arguments 对象,它仍然反映调用时传给函数的参数:
function getSum(...values) {
console.log(arguments.length); // 3
console.log(arguments); // [1, 2, 3]
console.log(values); // [1, 2, 3]
}
console.log(getSum(1,2,3));
函数声明与函数表达式
JavaScript 引擎在加载数据 时对它们(函数声明和函数表达式)是区别对待的。
- JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中 生成函数定义。
- 函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
函数声明提升(function declaration hoisting)
// 没问题
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
// 以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。
// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
// 上面的代码之所以会出错,是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLVJhrwP-1684760697447)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230521163919768.png)]
函数作为值
函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。
不 仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数。
// 第一个参数应该是一个函数,第二个参数应该是要传给这个函数的值。
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument);
}
// 任何函数都可以像下面这样作为参数传递
function add10(num) {
return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
function getGreeting(name) {
return "Hello, " + name;
}
let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2); // "Hello, Nicholas"
**Attention:**如果是访问函数而不是调用函数,那就必须不带括号, 所以传给 callSomeFunction()的必须是 add10 和 getGreeting,而不能是它们的执行结果。
**从一个函数中返回另一个函数也是可以的,而且非常有用:**sort->compare()
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
let data = [
{name: "Zachary", age: 28},
{name: "Nicholas", age: 29}
];
data.sort(createComparisonFunction("name"));
console.log(data[0].name); // Nicholas
data.sort(createComparisonFunction("age"));
console.log(data[0].name); // Zachary
函数内部
在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增 了 new.target 属性。
arguments
arguments 对象是一个类数组对象,包含调用函数时传入的所有参数。
这 个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。
虽然主要用于包含函数参数,但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的 指针:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
阶乘函数一般定义成递归调用的,就像上面这个例子一样。但是,这个函数要正确执行就必须保证函数名是 factorial,从而导致 了紧密耦合。使用 arguments.callee 就可以让函数逻辑与函数名解耦:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
//
重写之后的 factorial()函数已经用 arguments.callee 代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数:
let trueFactorial = factorial;
factorial = function() {
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
this
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在 网页的全局上下文中调用函数时,this 指向 windows):
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
定义在全局上下文中的函数 sayColor()引用了 this 对象。这个 **this 到底引用哪个对象必须到 函数被调用时才能确定。**因此这个值在代码执行的过程中可能会变。如果在全局上下文中调用 sayColor(),这结果会输出"red",因为 this 指向 window,而 this.color 相当于 window.color。 而在把 sayColor()赋值给 o 之后再调用 o.sayColor(),this 会指向 o,即 this.color 相当于 o.color,所以会显示"blue"。
在箭头函数中,this引用的是定义箭头函数的上下文。下面的例子演示了这一点。在对sayColor() 的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的:
window.color = 'red';
let o = {
color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'
在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将 回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文:
function King() {
this.royaltyName = 'Henry';
// this 引用 King 的实例
setTimeout(() => console.log(this.royaltyName), 1000);
}
function Queen() {
this.royaltyName = 'Elizabeth';
// this 引用 window 对象
setTimeout(function() { console.log(this.royaltyName); }, 1000);
}
new King(); // Henry
new Queen(); // undefined
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ml7boxuW-1684760697448)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230521173500385.png)]
caller
ECMAScript 5 也会给函数对象上添加一个属性:caller。虽然 ECMAScript 3 中并没有定义,但所 有浏览器除了早期版本的 Opera 都支持这个属性。这个属性引用的是调用当前函数的函数,或者如果是 在全局作用域中调用的则为 null。比如:
function outer() {
inner();
}
function inner() {
console.log(inner.caller);
}
outer();
以上代码会显示 outer()函数的源代码。这是因为 ourter()调用了 inner(),inner.caller 指向 outer()。如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值:
function outer() {
inner();
}
function inner() {
console.log(arguments.callee.caller);
}
outer();
// 在严格模式下访问 arguments.callee 会报错
// 严格模式下还有一个限制,就是不能给函数的 caller 属性赋值,否则会导致错误。
new.target
ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。
ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果**函数是正常调用的,则 new.target 的值是 undefined;**如果是使用 new 关键字调用的,则 new.target 将引用被调用的 构造函数。
function King() {
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
函数属性与方法
ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length 和 prototype。其中,length 属性保存函数定义的命名参数的个数。
function sayName(name) {
console.log(name);
}
function sum(num1, num2) {
return num1 + num2;
}
function sayHi() {
console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
prototype 属性也许是 ECMAScript 核心中最有趣的部分。prototype 是保存引用类型所有实例 方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上,进而由所有实 例共享。这个属性在自定义类型时特别重要。
函数还有两个方法:apply()和 call()。这两个方法都会以指定的 this 值来调用函数,即会设 置调用函数时函数体内 this 对象的值。apply()方法接收两个参数:函数内 this 的值和一个参数数组。第二个参数可以是 Array 的实例,但也可以是 arguments 对象。
function sum(num1, num2) {
return num1 + num2;
}
// callSum1()会调用 sum()函数,将 this 作为函数体内的 this 值(这里等于window,因为是在全局作用域中调用的)传入,同时还传入了 arguments 对象。
function callSum1(num1, num2) {
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); // 传入数组
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-plo1YSeb-1684760697448)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230521174622849.png)]
call()方法与 apply()的作用一样,只是传参的形式不同。第一个参数跟 apply()一样,也是 this 值,而剩下的要传给被调用函数的参数则是逐个传递的。换句话说,通过 call()向函数传参时,必须 将参数一个一个地列出来,比如:
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}
console.log(callSum(10, 10)); // 20
如果想直接传 arguments 对象或者一个数组,那就用 apply();否则,就用 call()。
apply()和 call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内 this 值的能力。考虑下面的例子:
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
// sayColor()是一个全局函数,如果在全局作用域中调用它,那么会显示"red"。这是因为 this.color 会求值为 window.color。
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关 心方法。
bind()方法会创建一个新的函数实例, 其 this 值会被绑定到传给 bind()的对象。比如:
window.color = 'red';
var o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
对函数而言,继承的方法 toLocaleString()和 toString()始终返回函数的代码。
继承的方法 valueOf()返回函数本身。
函数表达式
函数声明:
function functionName(arg0, arg1, arg2) {
// 函数体
}
函数声明的关键特点是函数声明提升
sayHi();
function sayHi() {
console.log("Hi!");
}
函数表达式:不具备变量提升
sayHi();
function sayHi() {
console.log("Hi!");
}
递归
普通递归函数:
function factorial(num) {
if(num <= 1) {
return 1;
}else {
return num * factorial(num - 1);
}
}
通过arguments实现:(严格模式下会出错)
function factorial(num) {
if(num <= 1) {
return 1;
}else {
return num * arguments.callee(num - 1);
}
}
严格模式下可使用命名函数表达式:
const factorial = (function f(num) {
if(num <= 1) {
return 1;
}else {
return num * f(num - 1);
}
})
尾递归调用
“尾调用”,即外部函数的返回值是一个内部函数的返回值:
function outerFunction() {
return innerFunction(); // 尾调用
}
ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。
ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其 销毁,则引擎就会那么做。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UtljUuBo-1684760697449)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230522113855220.png)]
尾调用优化的条件
尾调用优化的条件就是确定外部栈帧真的没有必要存在了。涉及的条件如下:
-
代码在严格模式下执行;
-
外部函数的返回值是对尾调用函数的调用;
-
尾调用函数返回后不需要执行额外的逻辑;
-
尾调用函数不是引用外部函数作用域中自由变量的闭包。
不符合尾调用优化的要求:
"use strict";
// 无优化:尾调用没有返回
function outerFunction() {
innerFunction();
}
// 无优化:尾调用没有直接返回
function outerFunction() {
let innerFunctionResult = innerFunction();
return innerFunctionResult;
}
// 无优化:尾调用返回后必须转型为字符串
function outerFunction() {
return innerFunction().toString();
}
// 无优化:尾调用是一个闭包
function outerFunction() {
let foo = 'bar';
function innerFunction() { return foo; }
return innerFunction();
}
符合尾调用优化条件的例子:
"use strict";
// 有优化:栈帧销毁前执行参数计算
function outerFunction(a, b) {
return innerFunction(a + b);
}
// 有优化:初始返回值不涉及栈帧
function outerFunction(a, b) {
if (a < b) {
return a;
}
return innerFunction(a + b);
}
// 有优化:两个内部函数都在尾部
function outerFunction(condition) {
return condition ? innerFunctionA() : innerFunctionB();
}
差异化尾调用和递归尾调用是容易让人混淆的地方。无论是递归尾调用还是非递归尾调用,都可以应用优化。引擎并不区分尾调用中调用的是函数自身还是其他函数。不过,这个优化在递归场景下的效 果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SRC6LGeS-1684760697449)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230522114608250.png)]
尾调用优化的代码
通过递归 计算斐波纳契数列的函数:
// 显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
// 优化
"use strict";
// 基础框架
function fib(n) {
return fibImpl(0, 1, n);
}
// 执行递归
function fibImpl(a, b, n) {
if (n === 0) {
return a;
}
return fibImpl(b, a + b, n - 1);
}
闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10);
作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9t2XskTi-1684760697450)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230522173601363.png)]
函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局 部活动对象会被销毁,内存中就只剩下全局作用域。不过,闭包就不一样了。
在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。
// 加粗的代码位于内部函数(匿名函数)中,其中引用了外部函数的变量 propertyName。在这个内部函数被返回并在其他地方被使用后,它仍然引用着那个变量。
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
let compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DjE0lsl3-1684760697450)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230522173755030.png)]**createComparisonFunction()**的 活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留 在内存中,直到匿名函数被销毁后才会被销毁:
// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就可以释放内存了
compareNames = null;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1x7dBzN-1684760697451)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230522174436401.png)]
this 对象
在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义,则 this 对象会在运 行时绑定到执行函数的上下文。
如果在全局函数中调用,则 this 在非严格模式下等于 window,在严 格模式下等于 undefined。
如果作为某个对象的方法调用,则 this 等于这个对象。
匿名函数在这种情 况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是 undefined。
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
return function() {
return this.identity;
};
}
};
console.log(object.getIdentityFunc()()); // 'The Window'
object.getIdentityFunc()()会立即调用这个返回的函数, 从而得到一个字符串。可是,此时返回的字符串是"The Winodw",即全局变量 identity 的值。为什么匿名函数没有使用其包含作用域(getIdentityFunc())的 this 对象呢?
每个函数在被调用时都会自动创建两个特殊变量:this 和 arguments。内部函数永 远不可能直接访问外部函数的这两个变量。但是,如果把 this 保存到闭包可以访问的另一个变量中, 则是行得通的。
在定义匿名函数之前,先把外部函数的 this 保存 到变量 that 中。然后在定义闭包时,就可以让它访问 that,因为这是包含函数中名称没有任何冲突的 一个变量。
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
let that = this;
return function() {
return that.identity;
};
}
};
console.log(object.getIdentityFunc()()); // 'My Object'
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFjS7TM1-1684760697451)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230522175245898.png)]
在一些特殊情况下,this 值可能并不是我们所期待的值。比如下面这个修改后的例子:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentity () {
return this.identity;
}
};
object.getIdentity(); // 'My Object'
(object.getIdentity)(); // 'My Object'
// 因为赋值表达式的值是函数本身,this 值不再与任何对象绑定,所以返回的是"The Window"
(object.getIdentity = object.getIdentity)(); // 'The Window'
一般情况下,不大可能像第二行和第三行这样调用对象上的方法。但通过这个例子,我们可以知道, 即使语法稍有不同,也可能影响 this 的值。
内存泄漏
IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制(第 4 章讨论过),所以 闭包在这些旧版本 IE 中可能会导致问题。
function assignHandler() {
let element = document.getElementById('someElement');
element.onclick = () => console.log(element.id);
}
以上代码创建了一个闭包,即 element 元素的事件处理程序(事件处理程序将在第 13 章讨论)。 而这个处理程序又创建了一个循环引用。匿名函数引用着 assignHandler()的活动对象,阻止了对 element 的引用计数归零。只要这个匿名函数存在,element 的引用计数就至少等于 1。也就是说, 内存不会被回收。
其实只要这个例子稍加修改,就可以避免这种情况,比如:
function assignHandler() {
let element = document.getElementById('someElement');
let id = element.id;
element.onclick = () => console.log(id);
element = null;
}
在这个修改后的版本中,闭包改为引用一个保存着 element.id 的变量 id,从而消除了循环引用。 不过,光有这一步还不足以解决内存问题。因为闭包还是会引用包含函数的活动对象,而其中包含 element。即使闭包没有直接引用 element,包含函数的活动对象上还是保存着对它的引用。因此,**必须再把 element 设置为 null。**这样就解除了对这个 COM 对象的引用,其引用计数也会减少,从而确 保其内存可以在适当的时候被回收。
立即调用的函数表达式
立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组 括号后面的第二组括号会立即调用前面的函数表达式。
(function() {
// 块级作用域
})();
使用 IIFE 可以模拟块级作用域,位于函数体作用域的变量就像是在块级作用域中一样。ECMAScript 5 尚未支持块级作用域,使用 IIFE 模拟块级作用域是相当普遍的。
// IIFE
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 抛出错误,因为它访问的变量是在 IIFE 内部定义的,在外部访问不到。
在 ECMAScript 6 以后,IIFE 就没有那么必要了,因为块级作用域中的变量无须 IIFE 就可以实现同 样的隔离。
// 内嵌块级作用域
{
let i;
for (i = 0; i < count; i++) {
console.log(i);
}
}
console.log(i); // 抛出错误
// 循环的块级作用域
for (let i = 0; i < count; i++) {
console.log(i);
}
console.log(i); // 抛出错误
说明 IIFE 用途的一个实际的例子,就是可以用它锁定参数值。比如:
// 渲染到页面上之后,点击每个<div>都会弹出元素总数。这是因为在执行单击处理程序时,迭代变量的值是循环结束时的最终值,即元素的个数。
let divs = document.querySelectorAll('div');
// 达不到目的! 这里使用 var 关键字声明了循环迭代变量 i,但这个变量并不会被限制在 for 循环的块级作用域内。
for (var i = 0; i < divs.length; ++i) {
divs[i].addEventListener('click', function() {
console.log(i);
});
}
// IIFE来实现
let divs = document.querySelectorAll('div');
for (var i = 0; i < divs.length; ++i) {
divs[i].addEventListener('click', (function(frozenCounter) {
return function() {
console.log(frozenCounter);
};
})(i));
}
// ES6来实现,可以让每次点击都显示正确的索引了
let divs = document.querySelectorAll('div');
for (let i = 0; i < divs.length; ++i) {
divs[i].addEventListener('click', function() {
console.log(i);
});
}
// 如果把变量声明拿到 for 循环外部,那就不行了。
let divs = document.querySelectorAll('div');
// 达不到目的!
let i;
for (i = 0; i < divs.length; ++i) {
divs[i].addEventListener('click', function() {
console.log(i);
});
}
私有变量
严格来讲,JavaScript 没有私有成员的概念,所有对象属性都公有的。不过,倒是有私有变量的概 念。
// 函数 add()有 3 个私有变量:num1、num2 和 sum。
function add(num1, num2) {
let sum = num1 + num2;
return sum;
}
任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的 变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。
**特权方法(privileged method)**是能够访问函数私有变量(及私有函数)的公有方法。在对象上有 两种方式创建特权方法。
第一种是在构造函数中实现,比如:
function MyObject() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
这个模式是把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访 问构造函数中定义的所有变量和函数的能力。
在这个例子中,变量 privateVariable 和函数 privateFunction()只能通过 publicMethod()方法来访问。在创建 MyObject 的实例后,没有办法 直接访问 privateVariable 和 privateFunction(),唯一的办法是使用 publicMethod()。
下面的例子所示,可以定义私有变量和特权方法,以隐藏不能被直接修改的数据:
function Person(name) {
this.getName = function() {
return name;
};
this.setName = function (value) {
name = value;
};
}
let person = new Person('Nicholas');
console.log(person.getName()); // 'Nicholas'
person.setName('Greg');
console.log(person.getName()); // 'Greg'
定义了两个特权方法:getName()和 setName()。每个方法都可以构造函 数外部调用,并通过它们来读写私有的 name 变量。在 Person 构造函数外部,没有别的办法访问 name。因为两个方法都定义在构造函数内部,所以它们都是能够通过作用域链访问 name 的闭包。私有变量 name 对每个 Person 实例而言都是独一无二的,因为每次调用构造函数都会重新创建一套变量和方法。 不过这样也有个问题:必须通过构造函数来实现这种隔离。
静态私有变量
特权方法也可以通过使用私有作用域定义私有变量和函数来实现。这个模式如下所示:
(function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 构造函数
MyObject = function() {};
// 公有和特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
匿名函数表达式创建了一个包含构造函数及其方法的私有作用域。首先定义的是私有变量和私有函数,然后又定义了构造函数和公有方法。
注意,这个模式定义的构造函数没有使用函数声明,使用的是函数表达式。函数声明会创建内部函数,在这里并不是必需的。
基于同样的原因(但操作相反),这里声明 MyObject 并没有使用任何关键字。因为不使用关键字声明的变量会创建在全局作用域中,所以 MyObject 变成了全局变量, 可以在这个私有作用域外部被访问。注意在严格模式下给未声明的变量赋值会导致错误。
这个模式与前一个模式的主要区别就是,私有变量和私有函数是由实例共享的。因为特权方法定义 在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域。
(function() {
let name = '';
Person = function(value) {
name = value;
};
Person.prototype.getName = function() {
return name;
};
Person.prototype.setName = function(value) {
name = value;
};
})();
let person1 = new Person('Nicholas');
console.log(person1.getName()); // 'Nicholas'
person1.setName('Matt');
console.log(person1.getName()); // 'Matt'
let person2 = new Person('Michael');
console.log(person1.getName()); // 'Michael'
console.log(person2.getName()); // 'Michael'
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jpiFRmqe-1684760697452)(F:/TheJob/systemLearning/javaScript%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%87%BD%E6%95%B0.assets/image-20230522184410117.png)]
模块模式
模块模式,则在一个单例对象上实现了相同的隔离和封装。单例对象(singleton)就是只有一个实例的 对象。
JavaScript 是通过对象字面量来创建单例对象的,如下面的例子所示:
let singleton = {
name: value,
method() {
// 方法的代码
}
};
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。模块模式 的样板代码如下:
let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
};
}();
模块模式使用了匿名函数返回一个对象。在匿名函数内部,首先定义私有变量和私有函数。之后, 创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。
let application = function() {
// 私有变量和私有函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 公共接口
return {
getComponentCount() {
return components.length;
},
registerComponent(component) {
if (typeof component == 'object') {
components.push(component);
}
}
};
}();
在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有的数据,而这些数据又可以 通过其暴露的公共方法来访问。以这种方式创建的每个单例对象都是 Object 的实例,因为最终单例都 由一个对象字面量来表示。
模块增强模式
另一个利用模块模式的做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类 型的实例,但又必须给它添加额外属性或方法的场景。
let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象
let object = new CustomType();
// 添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function() {
privateVariable++;
return privateFunction();
};
// 返回对象
return object;
}();
如果前一节的 application 对象必须是 BaseComponent 的实例,那么就可以使用下面的代码来 创建它:
let application = function() {
// 私有变量和私有函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 创建局部变量保存实例
let app = new BaseComponent();
// 公共接口
app.getComponentCount = function() {
return components.length;
};
app.registerComponent = function(component) {
if (typeof component == "object") {
components.push(component);
}
};
// 返回实例
return app;
}();
总结
函数是 JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而 让开发者可以更有效地使用函数。
- 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数。
- ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。
- JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符, 可以实现函数定义和调用的完全动态化。
- 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了 什么参数等信息。 JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。
- 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的 变量对象。 通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。
- 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
- 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
- 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都 会被销毁。
- 虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域 中定义的变量。
- 可以访问私有变量的公共方法叫作特权方法。
function() {
privateVariable++;
return privateFunction();
};
// 返回对象
return object;
}();
如果前一节的 application 对象必须是 BaseComponent 的实例,那么就可以使用下面的代码来 创建它:
```js
let application = function() {
// 私有变量和私有函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 创建局部变量保存实例
let app = new BaseComponent();
// 公共接口
app.getComponentCount = function() {
return components.length;
};
app.registerComponent = function(component) {
if (typeof component == "object") {
components.push(component);
}
};
// 返回实例
return app;
}();
总结
函数是 JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而 让开发者可以更有效地使用函数。
- 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数。
- ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。
- JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符, 可以实现函数定义和调用的完全动态化。
- 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了 什么参数等信息。 JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。
- 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的 变量对象。 通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。
- 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
- 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
- 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都 会被销毁。
- 虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域 中定义的变量。
- 可以访问私有变量的公共方法叫作特权方法。
- 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增 强模式在单例对象上实现。