函数

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)); // 6console.log(multiply.prototype); // undefinedconsole.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); // sum2let sum22 = function sum23(num1, num2) {    return num1 + num2;};/*let sum24 = sum22;console.log('24', sum24.name); // sum23sum24 = sum2;sum2 = null;console.log(sum24);console.log('24', sum24.name); // sum2*/console.log(sum22.name); // sum23// sum23(2, 3); // ReferenceError: sum23 is not definedconsole.log(sum4.name); // anonymousconsole.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, welcomefunction doAdd (num1, num2) {    arguments[1] = 10;    console.log( num2, arguments[0] + num2 );}doAdd(2, 3); // 10 12doAdd(2); // undefined NaNfunction 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); // 300let 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 Iconsole.log(makeKing('Louis', 'XVI')); // King Louis XVIconsole.log(makeKing()); // King Henry IIfunction 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)); // 10console.log(getSum(...values)); // 10console.log(getSum(-1, ...values)); // 9function getProduct(a, b, c = 1) {    return a * b *c;}let getSum2 = (a, b, c = 0) => {    return a + b + c;}console.log(getProduct(...[1, 2])); // 2console.log(getProduct(...[1, 2, 3])); // 6console.log(getProduct(...[1, 2, 3, 4])); // 6console.log(getSum2(...[0 ,1])); // 1console.log(getSum2(...[0 ,1, 2])); // 3console.log(getSum2(...[0 ,1, 2, 3])); // 3// (定义时)收集参数function getSum3 (...values) {    return values.reduce((x, y) => x + y, 0);}console.log( getSum3(1, 2, 3) ); // 6function 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); // 20function 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); // Nicholasdata.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)); // 120console.log(factorial(5)); // 0/*在浏览器中运行window.color = 'red';let o = {    color: 'blue'};let sayColor = () => console.log(this.color);sayColor(); // redo.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)); // 20console.log(callSum2(10, 10)); // 20console.log(callSum(10, 10)); // 20/*在浏览器中运行window.color = 'red';let o = {    color: 'blue'};function sayColor() { console.log(this.color);}sayColor(); // redsayColor.call(this); // redsayColor.call(window); // redsayColor.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)); // 2let anotherFactorial = factorial2;factorial2 = null;console.log(anotherFactorial(3)); // 6let 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。