关于前端:JavaScript高级程序设计笔记10-函数Function

3次阅读

共计 15860 个字符,预计需要花费 40 分钟才能阅读完成。

函数

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。

正文完
 0