JavaScript高级程序设计笔记10 函数Function

函数

1.几种实例化函数对象的方式

  • 以函数声明的方式定义

  • 函数表达式

  • 箭头函数(arrow function)

  • 使用Function构造函数

    接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。

    不推荐使用:这段代码会被解释两次,第一次是将它当作常规ECMAScript代码,第二次是解释传给构造函数的字符串。会影响性能。

    优点:可帮助理解函数作为对象,把函数名想象为指针

/* 实例化函数对象的方式 */
// 函数声明
function sum1 (num1, num2) {
	return num1 + num2;
}
console.log(sum1(1,2)); // 3
// 函数表达式
let sum2 = function (num1, num2) {
	return num1 + num2;
};
console.log(sum2(1,2)); // 3
// 箭头函数
let sum3 = (num1, num2) => {
	return num1 + num2;
};
console.log(sum3(1,2)); // 3
// 使用Function构造函数
let sum4 = new Function("num1", "num2", "num3", "return num1 + num2 + num3");
console.log(sum4(1, 2, 5)); // 8

2.箭头函数

ES6新增。

任何可以使用函数表达式的地方,都可以使用箭头函数。

简洁的语法非常适合嵌入函数的场景:

  • 如果只有一个参数,也不用圆括号;没有参数或者多个参数的情况下才需要括号
  • 也可以不用大括号:不用大括号,则箭头后面就只能有一行代码,如一个赋值操作、或者一个表达式,函数会隐式返回这行代码的值

缺点:

  • 不能使用arguments、super和new target
  • 也不能用作构造函数
  • 没有prototype属性
let multiply = (a, b) => a * b;
console.log(multiply(3, 2)); // 6
console.log(multiply.prototype); // undefined
console.log(multiply.name); // multiply
// new multiply(3, 2); // TypeError: multiply is not a constructor

3.函数名

函数名就是指向函数(对象)的指针。

使用不带括号的函数名会访问函数指针,而不会执行函数。

function sum(num1, num2) {
  return num1 + num2;
}
let anotherSum = sum;
sum = null;
anotherSum(1, 2); // 3

把sum设置为null之后,就切断了它与函数之间的关联。

ES6中所有函数对象都会暴露一个只读的name属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符、或者说是一个字符串化的变量名。

  • 普通的函数声明,.name是函数声明的名称
  • 匿名函数表达式和箭头函数赋值给变量后,获取变量.name,得到的是变量的名称(函数name属性被赋值为变量名称?),不赋值给变量直接获取.name,得到的是空字符串
  • 具名函数表达式赋值给变量后,获取变量.name,得到的是函数表达式的名称
  • 通过new Function创建的函数对象,不管获取赋值到的变量.name,还是直接获取.name((new Function()).name),得到的都是anonymous。

如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前面会加上一个前缀。

function foo() {}
foo.bind(null).name; // "bound foo"
let o = {
    years:1,
    get age() {
        return this.years;
    }
};
Object.getOwnPropertyDescriptor(o, "age").get.name; // "get age"
// 赋值函数表达式给变量后,获取变量的name属性
// 如果是匿名函数,输出变量名
// 如果是具名函数,输出函数名
console.log(sum2.name); // sum2
let sum22 = function sum23(num1, num2) {
	return num1 + num2;
};
/*let sum24 = sum22;
console.log('24', sum24.name); // sum23
sum24 = sum2;
sum2 = null;console.log(sum24);
console.log('24', sum24.name); // sum2*/
console.log(sum22.name); // sum23
// sum23(2, 3); // ReferenceError: sum23 is not defined

console.log(sum4.name); // anonymous
console.log((() => {}).name); // ''
console.log((new Function()).name); // anonymous

4.函数参数

定义和调用的完全动态化。

4.1 ECMAScript函数的参数

特点: ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。

原因: ECMAScript函数的参数在内部表现为一个数组。

函数被调用时,总会接收一个数组,但函数并不关心这个数组中包含什么。在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。

arguments对象 ,是一个类数组对象(不是Array的实例,是Object的实例)。要确定传进来多少个参数,可以访问arguments.length属性。

ECMAScript函数的参数只是为了方便才写出来的,并不是必须写出来的。ECMAScript中的命名参数不会创建让之后的调用必须匹配的函数签名。(因为根本不存在验证命名参数的机制)

可以根据arguments对象的情况编写不同的处理逻辑,虽然不像真正的函数重载那么明确,但这已经足以弥补ECMAScript在这方面的缺失了。

arguments对象可以跟命名参数一起使用。

arguments对象中的值,始终会与对应的命名参数同步。修改arguments对象中元素的值,会自动同步到对应的命名参数,反之亦然。(它们在内存中还是分开的,只不过会保持同步)

arguments对象的长度是根据传入的参数个数确定的(与定义时的命名参数个数无关)。如果只传了一个参数,然后把arguments[1]设置为某个值,那么这个值并不会反映到第二个命名参数。

对于命名参数,如果调用时没有传这个参数,那么它的值就是undefined。类似于定义了变量但没有初始化。

严格模式下,给arguments[1]赋值不会影响第二个命名参数的值,对第二个命名参数重新赋值也不会影响arguments[1]的值;尝试重写arguments对象会报错(read-only)。

箭头函数中的参数:传给函数的参数不能使用arguments关键字访问,只能通过定义的命名参数访问。可以用一个普通函数把箭头函数包装起来。还可使用扩展操作符收集参数。

4.2 没有重载

ECMAScript函数没有签名,因为参数是由包含0个或多个值得数组表示的。没有函数签名,自然也就没有重载。可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载。

4.3 默认参数值

在ECMAScript5.1及以前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,如果是则意味着没有传这个参数,就给它赋一个默认值。

ES6支持显式定义默认参数了。只要在函数定义中的参数后面用=就可以为参数赋一个默认值。

在使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。修改命名参数也不会影响arguments对象(与ES5严格模式一样),它始终以调用函数时传入的值为准。

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值。

函数的默认参数只有在函数被调用时才会求值,而且计算默认值的函数只有在调用函数但未传相应参数时才会被调用。

默认参数作用域与暂时性死区:

默认参数会按照定义它们的顺序依次被初始化。所以后定义默认值的参数可以引用先定义的参数。

参数初始化顺序遵循”暂时性死区“规则,即前面定义的参数不能引用后面定义的。

4.4 参数扩展与收集

扩展操作符,函数定义中的参数列表,充分利用ECMAScript的弱类型及参数长度可变的特点。

扩展参数:

(调用时)

把传入的一个数组扩展为一个参数列表。对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。(在之前,一般通过apply()方法实现)

对arguments对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值。

arguments对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数。

收集参数:

(定义时)

可以使用扩展操作符把不同长度的独立参数组合为一个数组。(类似arguments对象的构造机制,不过收集参数的结果会得到一个Array实例)。

收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数。

箭头函数虽然不支持arguments对象,但支持收集参数的定义方式。

使用收集参数并不影响arguments对象,它仍然反映调用时传给函数的参数。

/* 关于参数 */
function sayHi(name, message) {
	console.log( arguments );
	console.log("Hello " + name + ", " + message);
}
sayHi('lily', 'welcome');
// Arguments(2) ["lily", "welcome", callee: ƒ, Symbol(Symbol.iterator): ƒ]
	// 	0: "lily"
	// 	1: "welcome"
	// 	callee: ƒ sayHi(name, message)
	// 	length: 2
	// 	Symbol(Symbol.iterator): ƒ values()
	// 	__proto__: Object
// Hello lily, welcome

function doAdd (num1, num2) {
	arguments[1] = 10;
	console.log( num2, arguments[0] + num2 );
}
doAdd(2, 3); // 10 12
doAdd(2); // undefined NaN
function doAdd2 (num1, num2) {
	num2 = 10;
	console.log( arguments[1] );
	// arguments = { "0": 1, "1": 2, length: 2 };
}
doAdd2(2, 3); // 10

// 没有重载,定义两个同名函数只会覆盖
function addSomeNumber(num) {
	return num + 100;
}
function addSomeNumber(num, num1) {
	return num + 200;
}
let result = addSomeNumber(100);
console.log(result); // 300

let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI'];
let ordinary = 0;
function getNumerals() {
	return romanNumerals[ordinary++];
}
function makeKing(name = 'Henry', numerals = getNumerals()) {
	return `King ${name} ${numerals}`;
}
console.log(makeKing()); // King Henry I
console.log(makeKing('Louis', 'XVI')); // King Louis XVI
console.log(makeKing()); // King Henry II

function makeKing2(name = 'Henry', numerals = name) {
	return `King ${name} ${numerals}`;
}
console.log(makeKing2()); // King Henry Henry

// (调用时)扩展参数
let values = [1, 2, 3, 4];
function getSum() {
	// console.log( 'arguments.length', arguments.length );
	let sum = 0;
	for(let i = 0; i < arguments.length; ++ i) {
		sum += arguments[i];
	}
	return sum;
}
console.log(getSum.apply(null, values)); // 10
console.log(getSum(...values)); // 10
console.log(getSum(-1, ...values)); // 9

function getProduct(a, b, c = 1) {
	return a * b *c;
}
let getSum2 = (a, b, c = 0) => {
	return a + b + c;
}
console.log(getProduct(...[1, 2])); // 2
console.log(getProduct(...[1, 2, 3])); // 6
console.log(getProduct(...[1, 2, 3, 4])); // 6

console.log(getSum2(...[0 ,1])); // 1
console.log(getSum2(...[0 ,1, 2])); // 3
console.log(getSum2(...[0 ,1, 2, 3])); // 3

// (定义时)收集参数
function getSum3 (...values) {
	return values.reduce((x, y) => x + y, 0);
}
console.log( getSum3(1, 2, 3) ); // 6

function ignoreFirst(firstValue, ...values) {
	// console.log(arguments.length);
	console.log(values);
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1, 2); // [ 2 ]
ignoreFirst(1, 2, 3); // [ 2, 3 ]

let getSum4 = (...values) => values.reduce((x, y) => x + y, 0);
console.log( getSum4(1, 2, 3) ); // 6

5.函数声明与函数表达式(比对:提升)

函数声明提升(function declaring hoisting):JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。(JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部)

而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

6. 函数作为值(引用值)

函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。(其他函数的参数,其他函数的返回值)

可以把函数作为参数传给另一个函数,也可以在一个函数中返回另一个函数。

如果是访问函数而不是调用函数,就必须不带括号。

其他:默认情况下,数组的sort()方法要对数组元素执行toString(),然后再决定它们的顺序。

/* 函数作为引用值 */
function callSomeFunction(someFunc, someArg) {
	return someFunc(someArg);
}
function add10(num) {
	return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20

function createComparisonFunction(propertyName) {
	return function (obj1, obj2) {
		let value1 = obj1[propertyName];
		let value2 = obj2[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

7. 函数内部对象

ES5中,函数内部存在两个特殊的对象:arguments和this。ES6又新增了new.target属性。

  • arguments

    一个类数组对象,包含调用函数时传入的所有参数。

    arguments对象还有一个callee属性,是一个指向arguments对象所在函数的指针。使用arguments.callee就可以让函数逻辑与函数名解耦。(递归)

  • this

    在标准函数和箭头函数中有不同的行为。

    在标准函数中,与调用方式有关,必须到函数被调用时才能确定。在代码执行的过程中可能会变。

    在箭头函数中,this引用的是定义箭头函数的上下文;this会保留定义该函数时的上下文。

  • caller

    ES5给函数对象上添加的一个属性。

    这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。

    如果要降低耦合度,可以通过arguments.callee.caller来引用同样的值。

    严格模式下,访问caller和callee都会报错。让第三方代码无法检测同一上下文中运行的其他代码。

  • new.target

    检测函数是否使用new关键字调用

    如果不是,则为undefined;如果是,则new.target将引用被调用的构造函数。

// 函数内部的特殊对象
function factorial(num) {
	if(num <= 1) return 1;
	else {
		return num * arguments.callee(num - 1);
	}
}
let trueFactorial = factorial;
factorial = function () {
	return 0;
}
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
/*
在浏览器中运行
window.color = 'red';
let o = {
	color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // red
o.sayColor = sayColor;
o.sayColor(); // red
*/

function outer() {
	inner();
}
function inner() {
	// "use strict";
	// TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
	console.log(inner.caller);
	console.log(arguments.callee.caller);
	console.log(arguments.caller); // undefined
}
outer();
// [Function: outer]
// [Function: outer]

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(); // throw  'King must be instantiated using "new"';

8. 函数属性与方法

  • 属性

    • length:保存函数定义的命名参数的个数

    • prototype:保存引用类型所有实例方法的地方,进而由所有实例共享。(在自定义类型时特别重要)

      Object.getOwnPropertyDescriptor(foo, 'name');
      // {value: "foo", writable: false, enumerable: false, configurable: true}
      Object.getOwnPropertyDescriptor(foo, 'prototype');
      // {value: {…}, writable: true, enumerable: false, configurable: false}
      
  • 方法

    • apply、call

      以指定的this值来调用函数。

      apply接收两个参数:函数内的this值和一个参数数组(Array实例或arguments对象)

      call方法与apply的作用一样,只是传参的形式不同。第一个参数也是参数内this的值,剩下的是要传给被调用函数的参数列表。通过call向函数传参时,必须将参数一个一个地列出来。

      要使用哪个,完全取决于怎么给要调用的函数传参更方便。如果不用给被调用的函数传参,则使用哪个方法都一样。

      好处是,可以将任意对象设置为任意函数的作用域,这样对象就可以不用关心方法。

    • bind

      bind方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。

    • 继承的方法

      toLocaleString()和toString()始终返回函数的代码。具体格式因浏览器而异。

      valueOf()返回函数本身(无法new操作,也没有prototype)


function sum5(num1, num2) {
	return num1 + num2;
}
function callSum1(num1, num2) {
	return sum5.apply(this, arguments);
}
function callSum2(num1, num2) {
	return sum5.apply(this, [num1, num2]);
}
function callSum(num1, num2) {
	// return sum5.call(this, num1, num2);
	return sum5.call(this, ...arguments);
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
console.log(callSum(10, 10)); // 20
/*
在浏览器中运行
window.color = 'red';
let o = {
	color: 'blue'
};
function sayColor() {
 console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
*/

9. 函数表达式

函数表达式最常见的形式:(创建函数并赋值给变量)

let functionName = function(arg0, arg1, arg2) {
  // body...
};

这样创建的函数叫做匿名函数(anonymous function),因为function关键字后面没有标识符。(有时也被称为兰姆达函数)。

未赋值给其他变量的匿名函数的name属性是空字符串。

不建议以下的使用形式:

if(condition) {
  function sayHi() {
    // ...
  }
} else {
  function sayHi() {
    // ...
  }
}

JavaScript引擎会尝试将其纠正为适当的声明。各浏览器纠正这个问题的方式并不一致。(兼容)

把以上的函数声明换成函数表达式就没问题了。

任何时候,只要函数被当作值来使用,它就是一个函数表达式。(6)

10. 递归

通常的形式是一个函数通过名称调用自己。

但如果把这个函数赋值给其他变量,就可能会出问题。在写递归函数时使用arguments.callee可以避免这个问题(严格模式下不能用callee)。

arguments.callee就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用。是引用当前函数的首选。

严格模式下,可以使用命名函数表达式(named function expression)达到目的。如:

const factorial = (function f(num) {
  if(num < 1) {
    return 1;
  } else {
    return num * f(num - 1);
  }
});

即使把函数赋值给另一个变量,函数表达式的名称f也不变,因此递归调用不会有问题。

let factorial2 = (function f(num) {
	if(num <= 1) return 1;
	else return num * f(num - 1);
});
console.log(factorial2(2)); // 2
let anotherFactorial = factorial2;
factorial2 = null;
console.log(anotherFactorial(3)); // 6
let obj = {
	num: 3,
	factorial: (function f(num) {
		if(num <= 1) return 1;
		else return num * f(num-1);
		// else return num * this.factorial(num-1); 两个写法都可以
	})
};
console.log(obj.factorial(3)); // 6

11. 尾调用优化

ES6新增了一项内存管理优化机制,让JavaScript引擎在满足条件时可以重用栈帧。非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。

如:

function outerFunction() {
  return innerFunction(); // 尾调用
}

ES6优化之前,执行这个例子会在内存中发生如下操作:

1)执行到outerFunction函数体,第一个栈帧被推到栈上;

2)执行outerFunction函数体,到return语句。计算返回值必须先计算innerFunction;

3)执行到innerFunction函数体,第二个栈帧被推到栈上;

4)执行innerFunction函数体,计算其返回值;

5)将返回值传回outerFunction,然后outerFunction再返回值;

6)将栈帧弹出栈外。

ES6优化之后,发生如下操作:

1)执行到outerFunction函数体,第一个栈帧被推到栈上;

2)执行outerFunction函数体,到达return语句。为求值返回语句,必须先求值innerFunction;

3)引擎发现把第一个栈帧弹出栈外也没问题,因为innerFunction的返回值也是outerFunction的返回值;

4)弹出outerFunction的栈帧;

5)执行到innerFunction函数体,栈帧被推到栈上;

6)执行innerFunction函数体,计算其返回值;

7)将innerFunction的栈帧弹出栈外。

第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。第二种情况下,无论调用多少次嵌套函数,都只有一个栈帧。

ES6尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。

11.1 尾调用优化的条件

确定外部栈帧真的没有必要存在了。

涉及的条件如下:

  • 代码在严格模式下执行;
  • 外部函数的返回值是对尾调用函数的调用;
  • 尾调用函数返回后不需要执行额外的逻辑
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包。

差异化尾调用和递归尾调用。引擎并不区分尾调用中调用的是函数自身还是其他函数。但这个优化在递归场景下的效果最明显,因为递归代码最容易在栈内存中迅速产生大量栈帧。

非严格模式下,函数调用中允许使用f.arguments和f.caller,它们都会引用外部函数的栈帧。——意味着不能应用优化了。

11.2 尾调用优化的代码(例子)

斐波那契数列。

把简单的递归函数转换为待优化的代码(可优化的代码)。把递归改写成迭代循环形式。

12. 闭包

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。

在函数执行时,要从作用域链中查找变量,以便读、写值。

函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在;而函数局部上下文中的叫活动对象,只在函数执行期间存在。

🌰:

function compare(value1, value2) {
  if(value1 < value2) {
    return -1;
  } else if(value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}
let result = compare(5, 10);
  • 在定义compare()函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中;
  • 在调用compare()函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链;接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。不过,闭包不一样。

在一个函数内部定义的函数,会把包含它的函数的活动对象添加到自己的作用域链中。

外部函数返回一个匿名函数后,匿名函数的作用域链被初始化为包含外部函数的活动对象和全局变量对象;外部函数的活动对象并不能在它执行完毕后被销毁,因为匿名函数的作用域链中仍然有对它的引用。(外部函数自身执行上下文的作用域链会销毁,但是它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁)

更占内存,建议仅在十分必要时使用。

12.1 this

在闭包中使用this会让代码变复杂。如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。

每个函数在被调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但,如果把this保存到闭包可以访问的另一个变量中,则行得通。(或者在返回函数时使用bind(this))

一些特殊情况下,this值可能并不是我们所期待的值。如:

window.identity = 'The Window';
let object = {
    identity: 'My Object',
    getIdentityFunc() {
        return this.identity;
    }
};
object.getIdentityFunc(); // "My Object"
(object.getIdentityFunc)(); // "My Object"
(object.getIdentityFunc = object.getIdentityFunc)(); // "The Window"

虽然加了括号之后看起来是对一个函数的引用,但按照规范,object.getIdentityFunc(object.getIdentityFunc)是相等的。第三个是执行了一次赋值后,再调用赋值后的结果,因为赋值表达式的值是函数本身,this值不再与任何对象绑定,所以返回的是"The Window"

12.2 内存泄漏

在有些老版本的IE中,把HTML元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁。

必须把element设置为null,这样就解除了对这个COM对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。

/*
* 在浏览器中运行
let divs = document.querySelectorAll('div');
let i;
for(i = 0; i < divs.length; ++ i) {
	divs[i].addEventListener('click', function () {
		console.log(i);
	});
}
* */

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

类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。🌰:

(function() {
  // 块级作用域
})();

使用IIFE可以模拟块级作用域。即,在一个函数表达式内部声明变量,然后立即调用这个函数。

在ES5.1及以前,为了防止变量定义外泄,IIFE是个非常有效的方式。这样也不会导致闭包相关的内存问题,因为不存在对这个匿名函数的引用。为此,只要函数执行完毕,其作用域链就可以被销毁。

14.私有变量(利用闭包)

任何定义在函数或块中的变量(函数参数、局部变量、函数定义),都可以认为是私有的。因为在这个函数或块的外部无法访问其中的变量。

如果一个函数中创建了一个闭包,则这个闭包能通过其作用域链访问其外部的变量。基于这一点,就可以创建出能够访问私有变量的公有方法。

特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。创建特权方法的方式有两种,其一是在构造函数中实现。

// 把所有私有变量和私有函数都定义在构造函数中,
// 然后,再创建一个能够访问这些私有成员的特权方法。
function MyObject() {
  // 私有变量和私有函数
  let privateVariable = 10;
  
  function privateFunction() {
    return false;
  }
  // 特权方法
  this.publicMethod = function() {
    privateVariable ++;
    return privateFunction();
  };
}
let obj = new MyObject();
obj.publicMethod();

定义在构造函数中的特权方法其实是一个闭包,具有访问构造函数中定义的所有变量和函数的能力。

在这个例子中,实例无法直接访问私有成员,唯一的办法是使用publicMethod()。

每次调用构造函数都会重新创建一套变量和方法。=>每个实例都会重新创建一遍新方法。(类似第8章创建对象的构造模式)

14.1 静态私有变量(共享)

特权方法也可以通过使用私有作用域定义私有变量和函数来实现。

/*
	匿名函数表达式创建了一个包含构造函数及其方法的私有作用域。
	公有方法定义在构造函数的原型上,与典型的原型模式一样。
	(此处构造函数的定义,使用函数表达式而非函数声明,函数声明会创建内部函数,在此处不需要。且此处MyObject没有使用关键字声明,会被创建到全局作用域中,可以在这个私有作用域外部被访问。)
*/
(function() {
  // 私有变量和私有函数
  let privateVariable = 10;
  
  function privateFunction() {
    return false;
  }
  // 构造函数
  MyObject = function() {};
  // 公有和特权方法
  MyObject.prototype.publicMethod = function () {
    privateVariable ++;
    return privateFunction();
  }
})();

与通过构造函数实现的特权方法的主要区别,在于私有变量和私有函数是由实例共享的。因为特权方法定义在原型上,所以同样是由实例共享的。但是每个实例没有了自己的私有变量。

特权方法作为一个闭包,始终引用着包含它的作用域。

把私有变量放在实例中,还是作为静态私有变量,需要根据具体的需求来确定。

注:使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多。

14.2 模块模式

以上是通过自定义类型创建了私有变量和特权方法。

此处的模块模式,是在一个单例对象上实现相同的隔离和封装。

按照惯例,JavaScript是通过对象字面量来创建单例对象的。

模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。模块模式的样板代码如下:

let singleton = function() {
  // 私有变量和私有函数
  let privateVariable = 10;
  
  function privateFunction() {
    return false;
  }
  
  // 特权/公有方法和属性
  return {
    publicProperty: true,
    publicMethod() {
      privateVariable ++;
    	return privateFunction();
    }
  };
}();

模块模式使用了匿名函数返回一个对象。创建了一个要通过匿名函数返回的对象字面量。因为这个对象定义在匿名函数内部,所以它的所有公有方法都可以访问同一个作用域的私有变量和私有函数。本质上,对象字面量定义了单例对象的公共接口。

如果单例对象需要进行某种初始化,并且需要访问私有变量时,就可以采用这个模式。

在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有的数据,而这些数据又可以通过其暴露的公共方法来访问。以这种方式创建的每个单例对象都是Object的实例,因为最终单例都由一个对象字面量来表示。(通常也不作为参数传给函数,之类的)

14.3 模块增强模式

另一个利用模块模式的做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景。

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;
}();

创建了一个名为object的变量,其中保存了CustomType类型的实例;这是最终要变成singleton的那个对象的局部版本;给这个对象添加了能够访问私有变量的公共方法之后,匿名函数返回了这个对象。然后,这个对象被赋值给singleton。

posted @ 2022-10-13 21:14  beckyye  阅读(60)  评论(0编辑  收藏  举报