G
函数 Function , 函数的本质是对象, 每一个函数都是 Function的实例。 因为函数是对象,所以函数名就是指向函数对象的指针 。

函数的定义

函数声明

  1. function sum (num1, num2) {
  2. return num1 + num2;
  3. }

函数表达式

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

箭头函数

  1. let sum = (num1, num2) => {
  2. return num1 + num2;
  3. }

使用 Function构造函数创建

  1. let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐

箭头函数

ES6增加的 箭头函数, 使用 胖箭头 => 定义。 大程度上,很箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。

箭头函数的使用

  1. let arrowSum = (a, b) => {
  2. return a + b;
  3. };
  4. let functionExpressionSum = function(a, b) {
  5. return a + b;
  6. };
  7. console.log(arrowSum(5, 8)); // 13
  8. console.log(functionExpressionSum(5, 8)); // 13

箭头函数更简洁

  1. let ints = [1, 2, 3];
  2. console.log(ints.map(function(i) { return i + 1; })); // [2, 3, 4]
  3. console.log(ints.map((i) => { return i + 1 })); // [2, 3, 4]
  4. // 如果只有一个参数,那也可以不用括号。
  5. let double = (x) => { return 2 * x; };
  6. let triple = x => { return 3 * x; };
  7. // 没有参数需要括号
  8. let getRandom = () => { return Math.random(); };
  9. // 多个参数需要括号
  10. let sum = (a, b) => { return a + b; };
  11. // 省略return
  12. // 以下两种写法都有效,而且返回相应的值
  13. let double = (x) => { return 2 * x; };
  14. let triple = (x) => 3 * x;
  15. let triple = x => 3 * x;

箭头函数的不足
箭头函数不能使用argumentssupernew.target,也不能用作构造函数。此外,箭头函数也没有prototype 属性。

函数名

函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为 。 就说一个函数可以有多个名称

  1. function sum(num1, num2) {
  2. return num1 + num2;
  3. }
  4. console.log(sum(10, 10)); // 20
  5. let anotherSum = sum; // 多个指针
  6. console.log(anotherSum(10, 10)); // 20
  7. sum = null;
  8. console.log(anotherSum(10, 10)); // 20

ECMAScript 6 的所有函数对象都会暴露一个只读的name 属性,包含函数的信息

  • 一般函数属性 name , 返回函数名 (函数的唯一标识)
  • 箭头函数 (没有名称), 返回 空字符串
  • 如果它是使用Function 构造函数创建的,则会标识成”anonymous” ```javascript // 函数的 name 属性

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

  1. 如果使用 `bind()` , 则会在前面加上前缀 `bound foo`
  2. ```javascript
  3. function foo() {}
  4. console.log(foo.bind(null).name); // bound foo

理解参数 arguments

函数的参数内部表现为一个数组,在使用function 关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。

arguments 对象是一个类数组对象(但不是Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是arguments[0],第二个参数是arguments[1])。而要确定传进来多少个参数,可以访问arguments.length 属性。

  1. //
  2. function sayHi(name, message) {
  3. console.log("Hello " + name + ", " + message);
  4. }
  5. // 可以通过arguments[0]取得相同的参数值。因此,把函数重写成不声明参数也可以:
  6. function sayHi() {
  7. console.log("Hello " + arguments[0] + ", " + arguments[1]);
  8. }

也可以通过arguments 对象的length 属性检查传入的参数个数。

  1. function howManyArgs() {
  2. console.log(arguments.length);
  3. }
  4. howManyArgs("string", 45); // 2
  5. howManyArgs(); // 0
  6. howManyArgs(12); // 1

箭头函数中的参数

如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用arguments 关键字访问,而只能通过定义的命名参数访问。

  1. function foo() {
  2. console.log(arguments[0]);
  3. }
  4. foo(5); // 5
  5. let bar = () => {
  6. console.log(arguments[0]);
  7. };
  8. bar(5); // ReferenceError: arguments is not defined

虽然箭头函数中没有arguments 对象,但可以在包装函数中把它提供给箭头函数:

  1. function foo () {
  2. let bar = () => {
  3. console.log(arguments[0])
  4. }
  5. }
  6. foo(5) // 5

注意点: 函数的传递参数都是按值传递的。 不可能按照引用传递参数。 如果传递的参数是一个对象, 那么传递的值就是这个对象的引用(指针)

函数重载

ECMAScript 函数不能像传统编程那样重载。在其他语言比如Java 中,一个函数可以有两个定义,
只要签名(接收参数的类型和数量)不同就行 。

如前所述,ECMAScript 函数没有签名,因为参数是由 包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。 JS 函数没有重载

  1. 当定义两个同名函数, 则后面定义会覆盖先定义的
  1. function addSomeNumber(num) {
  2. return num + 100;
  3. }
  4. function addSomeNumber(num) {
  5. return num + 200;
  6. }
  7. let result = addSomeNumber(100); // 300

默认参数

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

  1. function makeKing(name = 'Henry') {
  2. return `King ${name} VIII`;
  3. }
  4. console.log(makeKing('Louis')); // 'King Louis VIII'
  5. console.log(makeKing()); // 'King Henry VIII'
  6. // 给参数传undefined 相当于没有传值,不过这样可以利用多个独立的默认值:
  7. function makeKing(name = 'Henry', numerals = 'VIII') {
  8. return `King ${name} ${numerals}`;
  9. }
  10. console.log(makeKing()); // 'King Henry VIII'
  11. console.log(makeKing('Louis')); // 'King Louis VIII'
  12. console.log(makeKing(undefined, 'VI')); // 'King Henry VI'

在使用默认参数时 , arguments 对象的值不反映参数的默认值, 只反映传给函数的参数

  • arguments 不会保存 默认参数
    1. function makeKing(name = 'Henry') {
    2. name = 'Louis';
    3. return `King ${arguments[0]}`;
    4. }
    5. console.log(makeKing()); // 'King undefined' / 不会识别默认参数
    6. console.log(makeKing('Louis')); // 'King Louis'

箭头函数使用默认参数

  1. let makeKing = (name = 'Henry') => `King ${name}`;
  2. console.log(makeKing()); // King Henry

参数扩展与收集

扩展参数

  1. let values = [1,2,3,4]
  2. function countArguments() {
  3. console.log(arguments.length);
  4. }
  5. countArguments(-1, ...values); // 5
  6. countArguments(...values, 5); // 5
  7. countArguments(-1, ...values, 5); // 6
  8. countArguments(...values, ...[5,6,7]); // 7

函数声明和函数表达式

  1. // 函数声明
  2. function sum() {}
  3. // 函数表达式
  4. let sum = function () {}

区别

  1. 函数声明会被提升 : 在 JS引擎读取代码之前,会把函数声明先读取后添加到执行上下文 , 而函数表达式不会 ```javascript // 没问题 console.log(sum(10, 10)); function sum(num1, num2) { // 函数声明 return num1 + num2; }

// 会出错 console.log(sum(10, 10)); // 这并不是因为使用let 而导致的,使用var 关键字也会碰到同样的问题 let sum = function(num1, num2) { // 函数表达式 return num1 + num2; };

  1. <a name="NWyLQ"></a>
  2. ## 函数作为值
  3. 这是一种重构代码的方式 , 让代码更佳可读性
  4. 定义函数,第一个参数应该是一个函数,第二个参数应该是要传给这个函数的值
  5. ```javascript
  6. // 定义重构函数
  7. function callSomeFunction (someFunction, someArgument) {
  8. // someFunction -> 函数名
  9. // someArgument -> someFunction这个函数的参数
  10. // callSomeFunction 函数逻辑 -> 返回 someFunction(someArgument) 的返回值
  11. return someFunction(someArgument)
  12. }
  13. // 使用
  14. function sum10 (num) {
  15. return num + 10
  16. }
  17. let result1 = callSomeFunction(add10, 10);
  18. console.log(result1); // 20
  19. function getGreeting(name) {
  20. return "Hello, " + name;
  21. }
  22. let result2 = callSomeFunction(getGreeting, "Nicholas");
  23. console.log(result2); // "Hello, Nicholas"

callSomeFunction()函数是通用的,第一个参数传入的是什么函数都可以,而且它始终返回调用作为第一个参数传入的函数的结果。

高级函数

从一个函数中返回另一个函数也是可以的,而且非常有用 。

  1. // 定义一个高阶函数
  2. function createComparisonFunction(propertyName) {
  3. // 返回一个函数
  4. return function (obj1, obj2) {
  5. // 处理
  6. let value1 = object1[propertyName];
  7. let value2 = object2[propertyName];
  8. if (value1 < value2) {
  9. return -1;
  10. } else if (value1 > value2) {
  11. return 1;
  12. } else {
  13. return 0;
  14. }
  15. }
  16. }
  17. let data = [
  18. {name: "Zachary", age: 28},
  19. {name: "Nicholas", age: 29}
  20. ];
  21. data.sort(createComparisonFunction("name"))
  22. console.log(data[0].name); // Nicholas
  23. data.sort(createComparisonFunction("age"));
  24. console.log(data[0].name); // Zachary

函数内部

函数内部存在两个特殊的对象 : arguments | this , ES6 新增 new.target

arguments

  • arguments 它不是数组, 而是类数组 , 包含调用函数时传入的所有参数
  • 只有 function 定义的函数,才会有 arguments
  • argument 还有一个属性 callee , 通过arguments.callee() 能够访问到 当前的函数指针,也就是能够调用当前的函数 ```javascript // 阶层函数 -> 目的:递归调用 function factorial(num) { if (num <= 1) { return 1; } else { // 递归 return num * factorial(num - 1); } }

// 使用arguments.callee 就可以让函数逻辑与函数名解耦 function factorial(num) { if (num <= 1) { return 1; } else { // 这里使用 arguments.callee() 而不是递归调用 return num * arguments.callee(num - 1); } }

  1. <a name="enfXt"></a>
  2. ## this
  3. this 在标准函数 和 箭头函数 this 的指向不同
  4. 标准函数 :
  5. - this 引用的是**把函数当成方法调用的上下文对象**,这时候通常称其为this 值(在网页的全局上下文中调用函数时,this 指向windows)。
  6. ```javascript
  7. window.color = 'red';
  8. let o = {
  9. color: 'blue'
  10. };
  11. function sayColor() {
  12. console.log(this.color);
  13. }
  14. sayColor(); // 'red' // 调用这个函数 就是 sayColor()
  15. o.sayColor = sayColor;
  16. o.sayColor(); // 'blue' // 是 o 调用了这个函数

箭头函数 :

  • this 引用的是定义箭头函数的上下文
    1. window.color = 'red';
    2. let o = {
    3. color: 'blue'
    4. };
    5. let sayColor = () => console.log(this.color);
    6. sayColor(); // 'red'
    7. o.sayColor = sayColor;
    8. o.sayColor(); // 'red'

new.target

因为 ECMAScript 中的函数可以作为构造函数,并实例化为一个新对象 , 也可以做为普通对象被调用。

在ES6新增的 new.target 用来检测 函数是否使用了 new 关键字。 如果使用了 new,则触发 new.target 属性 。 如果函数是普通正常调用的 , new.targetundefined 。 如果是使用new 关键字调用的,则new.target 将引用被调用的构造函数 (constrator)。

  1. function King() {
  2. if (!new.target) {
  3. throw 'King must be instantiated using "new"'
  4. }
  5. console.log('King instantiated using "new"');
  6. }
  7. new King(); // King instantiated using "new"
  8. King(); // Error: King must be instantiated using "new"

函数属性与方法

函数属性

因为函数的本质也是对象 , 也具有 属性 和方法
其中函数中具有的两个属性 : length & prototype

  • length : 保存函数参数的个数
  • prototype : 保存引用类型所有实例 , 这意味着toString()、valueOf()等方法实际上都保存在prototype 上,

length

  1. function sayName(name) {
  2. console.log(name);
  3. }
  4. function sum(num1, num2) {
  5. return num1 + num2;
  6. }
  7. function sayHi() {
  8. console.log("hi");
  9. }
  10. console.log(sayName.length); // 1
  11. console.log(sum.length); // 2
  12. console.log(sayHi.length); // 0

prototype

  • 保存了 toString()、valueOf()等方法 -> 函数的prototype 指向了 实例的 __proto__
  • prototype 属性是不可枚举的,因此使用for-in 循环不会返回这个属性。 ```javascript function a () {} a.length // 0 a.prototype

a.prototype === Function // false let b = new a () b === a.prototype // false b.proto === a.prototype // true

  1. <a name="CO2Af"></a>
  2. ## 函数方法
  3. 函数方法 有 : `apply() & call()` , 这两个方法都会以指定的this 值来调用函数 , 会设置函数调用函数时函数体内的 this对象值 。
  4. `apply()` 接收两个参数 函数内this 的值和一个参数数组
  5. 1. this 的指向
  6. 2. 函数的参数, 这里参数是一个数组
  7. ```javascript
  8. function sum(num1, num2) {
  9. return num1 + num2;
  10. }
  11. function callSum1(num1, num2) {
  12. // 这里调用 sum() , 传入 this 也就是 callSum1, arguments 是一个数组
  13. return sum.apply(this, arguments); // 传入arguments 对象
  14. }
  15. function callSum2(num1, num2) {
  16. return sum.apply(this, [num1, num2]); // 传入数组
  17. }
  18. console.log(callSum1(10, 10)); // 20
  19. console.log(callSum2(10, 10)); // 20

在严格模式下,调用函数时如果没有指定上下文对象,则this 值不会指向window。 除非使用apply()或call()把函数指定给一个对象,否则this 的值会变成undefined。

call()方法与apply()的作用一样,只是传参的形式不同
call() 的参数

  1. this 的指向
  2. 传递参数, 要传给被调用函数的参数则是逐个传递的 。 也即是传递参数要 一个一个列出来
    1. function sum(num1, num2) {
    2. return num1 + num2;
    3. }
    4. function callSum(num1, num2) {
    5. return sum.call(this, num1, num2);
    6. }
    7. console.log(callSum(10, 10)); // 20

apply()和call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内this 值的能力。

  1. window.color = 'red';
  2. let o = {
  3. color: 'blue'
  4. };
  5. // 全局函数 -> this 指向 window
  6. function sayColor() {
  7. console.log(this.color);
  8. }
  9. sayColor(); // red
  10. sayColor.call(this); // red , 这里的 this是 window
  11. sayColor.call(window); // red
  12. sayColor.call(o); // blue , 这里的 this 指向 o

使用call()或apply()的好处是可以将任意对象设置为任意函数的作用域

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

  1. window.color = 'red';
  2. var o = {
  3. color: 'blue'
  4. };
  5. function sayColor() {
  6. console.log(this.color);
  7. }
  8. // 会创建 一个新的函数实例 objectSayColor
  9. // 这里函数实例 objectSayColor , 的 this 值 是由 bind() 指向的 o
  10. let objectSayColor = sayColor.bind(o);
  11. objectSayColor(); // blue
  12. // 即使是在全局作用域中调用 objectSayColor ,也会返回字符串"blue"。

apply() & call() & bind() 的区别
相同

  • 都能够改变 this 的指向
  • 调用方式相同, 都是函数名.函数方法()的形式调用

不同

  • apply & call:在调用时传参的不同, apply传入数组,call 传入单独的值
  • bind & apply call的不同: bind 在调用后,返回出来一个函数, 这个函数就是 bind方法创建后this 指向的函数

注意:

  • 在严格模式下, 在全局调用 call & apply 这样的函数方法,this 指向的不是window 而是会变为 undefined

函数表达式

函数表达式

  1. function functionName(arg0, arg1, arg2) {
  2. // 函数体
  3. }

函数声明的关键特点是 **函数声明的提升** 即函数声明会在代码执行之前获得定义

  1. sayHi();
  2. function sayHi() {
  3. console.log("Hi!");
  4. }
  5. // 不会报错 , 因为JavaScript 引擎会先读取函数声明 再执行代码

函数表达式

  1. let functionName = function(arg0, arg1, arg2) {
  2. // 函数体
  3. };
  1. 函数表达本质是 创建一个函数把他赋值给一个变量, 这样的方式也叫 匿名函数
  2. 函数表达式 没有 函数的提升, 不会提升
    1. sayHi(); // Error! function doesn't exist yet
    2. let sayHi = function() {
    3. console.log("Hi!");
    4. };

函数声明和 函数表达式的主要区别就是 函数声明的变量提升

递归函数

递归函数: 就是一个函数,通过它的名称来 自己调用自己

  1. // 递归阶层函数
  2. function factorial(num) {
  3. if (num <= 1) {
  4. return 1;
  5. } else {
  6. return num * factorial(num - 1);
  7. }
  8. }

虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题

  1. // 接上面代码
  2. let anotherFactorial = factorial;
  3. factorial = null;
  4. console.log(anotherFactorial(4)); // 报错
  5. /*
  6. * 这里把factorial()函数保存在了另一个变量anotherFactorial 中,然后将factorial 设置
  7. 为null,于是只保留了一个对原始函数的引用。而在调用anotherFactorial()时,要递归调用
  8. factorial(),但因为它已经不是函数了,所以会出错。在写递归函数时使用arguments.callee 可
  9. 以避免这个问题。
  10. */
  11. // 因为 arguments.callee 就是一个指向正在执行的函数的指针
  12. function factorial(num) {
  13. if (num <= 1) {
  14. return 1;
  15. } else {
  16. return num * arguments.callee(num - 1);
  17. }
  18. }
  19. let anotherFactorial = factorial;
  20. factorial = null;
  21. console.log(anotherFactorial(4));

尾调用的优化

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

  1. function outerFunction() {
  2. return innerFunction(); // 尾调用
  3. }
  4. // 在 ES5 前, 函数调用栈执行时 过程
  5. (1) 执行到outerFunction 函数体,第一个栈帧被推到栈上。
  6. (2) 执行outerFunction 函数体,到return 语句。计算返回值必须先计算innerFunction
  7. (3) 执行到innerFunction 函数体,第二个栈帧被推到栈上。
  8. (4) 执行innerFunction 函数体,计算其返回值。
  9. (5) 将返回值传回outerFunction,然后outerFunction 再返回值。
  10. (6) 将栈帧弹出栈外
  11. // 在 ES6 优化后, 内存发生了优化改变
  12. (1) 执行到outerFunction 函数体,第一个栈帧被推到栈上。
  13. (2) 执行outerFunction 函数体,到达return 语句。为求值返回语句,必须先求值innerFunction
  14. (3) 引擎发现把第一个栈帧弹出栈外也没问题,因为innerFunction 的返回值也是outerFunction的返回值。
  15. (4) 弹出outerFunction 的栈帧。
  16. (5) 执行到innerFunction 函数体,栈帧被推到栈上。
  17. (6) 执行innerFunction 函数体,计算其返回值。
  18. (7) innerFunction 的栈帧弹出栈外。

很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多
少次嵌套函数,都只有一个栈帧。这就是ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其
销毁,则引擎就会那么做。

尾调用的优化条件

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

下面展示了几个违反上述条件的函数,因此都不符号尾调用优化的要求:

  1. "use strict";
  2. // 无优化:尾调用没有返回
  3. function outerFunction() {
  4. innerFunction();
  5. }
  6. // 无优化:尾调用没有直接返回
  7. function outerFunction() {
  8. let innerFunctionResult = innerFunction();
  9. return innerFunctionResult;
  10. }
  11. // 无优化:尾调用返回后必须转型为字符串
  12. function outerFunction() {
  13. return innerFunction().toString();
  14. }
  15. // 无优化:尾调用是一个闭包
  16. function outerFunction() {
  17. let foo = 'bar';
  18. function innerFunction() { return foo; }
  19. return innerFunction();
  20. }

下面是几个符合尾调用优化条件的例子:

  1. "use strict";
  2. // 有优化:栈帧销毁前执行参数计算
  3. function outerFunction(a, b) {
  4. return innerFunction(a + b);
  5. }
  6. // 有优化:初始返回值不涉及栈帧
  7. function outerFunction(a, b) {
  8. if (a < b) {
  9. return a;
  10. }
  11. return innerFunction(a + b);
  12. }
  13. // 有优化:两个内部函数都在尾部
  14. function outerFunction(condition) {
  15. return condition ? innerFunctionA() : innerFunctionB();
  16. }

尾调用优化的代码

通过递归 计算斐波纳契数列的函数:

  1. function fib(n) {
  2. if (n < 2) {
  3. return n;
  4. }
  5. return fib(n - 1) + fib(n - 2);
  6. }
  7. console.log(fib(0)); // 0
  8. console.log(fib(1)); // 1
  9. console.log(fib(2)); // 1
  10. console.log(fib(3)); // 2
  11. console.log(fib(4)); // 3
  12. console.log(fib(5)); // 5
  13. console.log(fib(6)); // 8
  14. // 显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。

优化后

  1. "use strict";
  2. // 基础框架
  3. function fib(n) {
  4. return fibImpl(0, 1, n);
  5. }
  6. // 执行递归
  7. function fibImpl(a, b, n) {
  8. if (n === 0) {
  9. return a;
  10. }
  11. return fibImpl(b, a + b, n - 1);
  12. }

闭包

闭包的定义: 引用了另一个函数作用域的变量, 叫闭包; 也就是 一个变量,能够在两个不同的作用域之间相互使用
闭包的作用: 局部作用域共享

  1. function createComparisonFunction(propertyName) { // propertyName 参数1
  2. // 返回一个函数
  3. return function(object1, object2) {
  4. // 这个函数内使用了 propertyName ; 也就是另一个函数的变量
  5. // propertyName 就是一个闭包变量
  6. let value1 = object1[propertyName];
  7. let value2 = object2[propertyName];
  8. if (value1 < value2) {
  9. return -1;
  10. } else if (value1 > value2) {
  11. return 1;
  12. } else {
  13. return 0;
  14. }
  15. };
  16. }

函数调用时 作用域链的关系

代码执行原理:createComparisonFunction 执行完后,返回一个匿名函数; 此时执行 createComparisonFunction() 完,它的作用域还没有被清除,需要等匿名函数执行完后才清除;
而此时匿名函数使用到了它的 propertyName, 这个变量 匿名函数的作用域链中仍然有对它的引用 , 所以createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁

  1. // 创建比较函数
  2. let compareNames = createComparisonFunction('name');
  3. // 调用函数
  4. let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
  5. // 解除对函数的引用,这样就可以释放内存了
  6. compareNames = null;

闭包中的 this

闭包中的 this 比较复杂

  • 内部函数使用了 箭头函数 : this 指向运行时绑定到执行函数的上下文
  • 在全局函数中调用, 严格模式 -> this = undefined ; 非严格模式 this => window
    1. window.identity = 'The Window';
    2. let object = {
    3. identity: 'My Object',
    4. getIdentityFunc() {
    5. return function() {
    6. return this.identity;
    7. };
    8. }
    9. };
    10. console.log(object.getIdentityFunc()()); // 'The Window'
    每个函数在被调用时都会自动创建两个特殊变量:this 和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this 保存到闭包可以访问的另一个变量中,则是行得通的。

把this保存到闭包中

  1. window.identity = 'The Window';
  2. let object = {
  3. identity: 'My Object',
  4. getIdentityFunc() {
  5. // 先把外部函数的this 保存到变量that 中。
  6. // 这里 this 执行 obj
  7. let that = this
  8. return function() {
  9. // 通过 that 访问变量
  10. return that.identity;
  11. };
  12. }
  13. };
  14. console.log(object.getIdentityFunc()()); // 'My Object'

内存泄漏

  1. function assignHandler() {
  2. let element = document.getElementById('someElement');
  3. // 声明一个匿名函数
  4. element.onclick = () => console.log(element.id); // element.id 闭包变量
  5. }
  6. // 创建了一个闭包
  7. // 匿名函数引用着assignHandler()的活动对象,阻止了对element 的引用计数归零
  8. // 只要这个匿名函数存在,element 的引用计数就至少等于1。也就是说,内存不会被回收。
  1. // 修改
  2. function assignHandler() {
  3. let element = document.getElementById('someElement');
  4. let id = element.id;
  5. element.onclick = () => console.log(id);
  6. // 手动释放内存
  7. element = null;
  8. }

立即调用函数

立即调用的匿名函数又被称作立即调用的函数表达式 ; (() => {})(); 也叫 IIFE

  1. // 理解调用函数的声明
  2. (function () {
  3. // 块级作用域
  4. })()
  1. // IIFE
  2. (function () {
  3. for (var i = 0; i < count; i++) {
  4. console.log(i);
  5. }
  6. })();
  7. console.log(i); // 抛出错误, 访问不到 i

在ES6 后,IIFE 就没那么必要了 , 因为跨级作用 可以使用{} 隔离

  1. {
  2. let i;
  3. for (i = 0; i < count; i++) {
  4. console.log(i);
  5. }
  6. }
  7. console.log(i); // 抛出错误
  8. // 循环的块级作用域
  9. // 使用 let
  10. for (let i = 0; i < count; i++) {
  11. console.log(i);
  12. }
  13. console.log(i); // 抛出错误

私有变量

JavaScript 没有私有成员的概念,所有对象属性都公有的。
任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。

  1. // 特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。
  2. function MyObject() {
  3. // 私有变量
  4. let privateVariable = 10;
  5. // 私有函数
  6. function privateFunction() {
  7. return false;
  8. }
  9. // 特权方法 publicMethod
  10. this.publicMethod = function() {
  11. privateVariable++;
  12. return privateFunction();
  13. };
  14. }
  1. function Person(name) {
  2. this.getName = function() {
  3. return name;
  4. };
  5. this.setName = function (value) {
  6. name = value;
  7. };
  8. }
  9. let person = new Person('Nicholas');
  10. console.log(person.getName()); // 'Nicholas'
  11. person.setName('Greg');
  12. console.log(person.getName()); // 'Greg'
  13. // 两个特权方法:getName()和setName()

静态私有变量

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

  1. (function() {
  2. // 私有变量和私有函数
  3. let privateVariable = 10;
  4. function privateFunction() {
  5. return false;
  6. }
  7. // 构造函数
  8. MyObject = function() {};
  9. // 公有和特权方法
  10. // 在原型上设置 特权方法 publicMethod
  11. MyObject.prototype.publicMethod = function() {
  12. privateVariable++;
  13. return privateFunction();
  14. };
  15. })();

函数总结

  1. 函数声明和函数表达式不一样, 函数声明要求写函数名称 , 函数表达式不需要(匿名函数)
  2. ES6 增加了 箭头函数 , 需要记住箭头函数的特性
  3. ES6 新增用于 代替 argument 的, 扩展运算符 ...
  4. JS 引擎会对 符合优化的 尾调用条件的函数,节省栈空间
  5. 闭包的作用, 闭包的作用域链 , 普通函数的变量在执行完被销毁,闭包变量作用域一直保存在内存中,知道闭包被销毁
  6. 立即调用函数 ,执行完后不会留下对函数的应用
  7. 函数的属性方法,apply & bind & call 可以改变 this 的指向
  8. 私有变量|属性 的实现,使用闭包实现公共方法,访问位于包含作用域中定义的变量
  9. 访问私有变量中的公共方法叫做特权方法
  10. 特权方法可以通过 构造函数 & 原型模式 通过自定义类型中实现