title: JavaScript高级程序设计第四版学习--第十章


title: JavaScript高级程序设计第四版学习–第十章
date: 2021-5-22 19:04:26
author: Xilong88
tags: JavaScript

本章内容
函数表达式、函数声明及箭头函数
默认参数及扩展操作符
使用函数实现递归
使用闭包实现私有变量
可能出现的面试题:
1.常见的函数定义方式有哪些?
2.了解arguments吗?arguments.callee?
3.了解箭头函数吗?(聊聊ES6新特性)
4.了解默认参数吗?默认参数暂时性死区?(ES5中可以实现,也有ES6新特性)
5.怎么查看函数名?
6.了解扩展参数吗?了解收集参数吗?(聊聊ES6新特性)
7.了解this吗?如何改变this指向?apply和call的区别?bind?
8.如何判断函数被调用是有没有new?(了解new.target吗?)
9.arguments.callee可以用于递归解耦,假如在严格模式下怎么办?
10.了解尾调用优化吗?(在递归算法中使用尾调用优化,无形秀肌肉)
11. 了解闭包吗?原理?应用?内存泄漏?
12. 私有变量如何实现,场景,优点,缺点?
13. 静态私有变量如何实现,场景,缺点?
14. 了解过模块模式吗?场景?
15. 了解过模块增强模式吗?场景?

总结:这章可能涉及面试题有点多

这章主要讲了函数的基本知识,包括函数的几种定义方法,函数的一些属性,以及函数尾调用优化等。
还讲了闭包的原理,以及闭包的内存泄漏;
然后是几种编程的模式,主要是实现一些面向对象中的编程模式,比如私有变量,这里提到了私有变量,静态私有变量,模块模式和增强模块模式,都是很强的编程模式,在一些框架里面应该可以看到,学好了这章有利于后面学框架时理解框架的模式。

知识点:
1.函数定义的方式(常见的4种):
函数声明:

function sum (num1, num2) {
  return num1 + num2;
}

函数表达式:

let sum = function(num1, num2) {
  return num1 + num2;
};

箭头函数:

let sum = (num1, num2) => {
  return num1 + num2;
};

Function 构造函数:

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

注意,函数声明后不加分号,函数表达式后面要加分号,也就是赋值语句加分号

2.箭头函数

参数可以不填,大括号也可以不写,参数不填,但是要写括号,假如只有一个参数可以不写括号,假如返回值就是一个表达式,可以不要后面的大括号和return,默认返回后面的表达式。

// 以下两种写法都有效,而且返回相应的值
let double = (x) => { return 2 * x; };
let triple = (x) => 3 * x;
// 可以赋值
let value = {};
let setName = (x) => x.name = "Matt";
setName(value);
console.log(value.name); // "Matt"

// 无效的写法:
let multiply = (a, b) => return a * b;

没有大括号就不写return

箭头函数不能使用arguments 、super 和new.target ,也不能用作构造函数。此外,箭头函数也没有prototype 属性。

3.函数名

函数的名称就是一个指针,所以可以赋值给其他函数,函数对象有一个只读属性,name,里面存的最原始的函数名称,如果用的构造函数,name会显示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

为什么说最原始的函数名,看下面的例子:

let bar = function() {};
let baz = bar;
bar = null;
console.log(baz.name);   // bar

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

function foo() {}
console.log(foo.bind(null).name);    // bound foo
let dog = {
  years: 1,
  get age() {
    return this.years;
  },
  set age(newAge) {
    this.years = newAge;
  }
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name);  // get age
console.log(propertyDescriptor.set.name);  // set age

4.参数
ECMAscript 参数个数是不限制的,也就是说,可以是零个,也可以是多个,在函数定义时写入的参数列表,只是为了方便。最终传入的参数都会在一个arguments对象里面,这是一个类数组对象,所以可以用中括号访问元素。.length可以查看元素个数。

arguments 对象可以跟命名参数一起使用,它的值始终会与对应的命名参数同步。

function doAdd(num1, num2) {
  arguments[1] = 10;
  console.log(arguments[0] + num2);
}

它们在内存中还是分开的,只不过会保持同步而已,也就是说,命名参数和arguments是分开的,但是会同步

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

对于命名参数而言,如果调用函数时没有传这个参数,那么它的值就是undefined

其次,在函数中尝试重写arguments 对象会导致语法错误。代码也不会执行。

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

function foo() {
  let bar = () => {
    console.log(arguments[0]); // 5
  };
  bar();
}
foo(5);

5.没有重载

(没有自带的重载,但是可以通过一些方法实现,这里暂时没讨论)

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

如果在ECMAScript中定义了两个同名函数,则后定义的会覆盖先定义的。来看下面的例子:

function addSomeNumber(num) {
  return num + 100;
}
function addSomeNumber(num) {
  return num + 200;
}
let result = addSomeNumber(100); // 300

6.默认参数值

ES5和之前,用三元表达式来做默认参数:

function makeKing(name) {
  name = (typeof name !== 'undefined') ? name : 'Henry';
  return `King ${name} VIII`;
}
console.log(makeKing());         // 'King Henry VIII'
console.log(makeKing('Louis'));  // 'King Louis VIII'

ES6之后可以直接在参数列表里写默认参数:

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'

也就是说,比如我想利用第二个默认值,我就可以把第一个参数传值为undefined。

使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。当然,跟ES5严格模式一样,修改命名参数也不会影响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'
console.log(makeKing('Louis', 'XVI'));  // 'King Louis XVI'
console.log(makeKing());                // 'King Henry II'
console.log(makeKing());                // 'King Henry III'

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用。箭头函数同样也可以这样使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了:

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

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

给多个参数定义默认值实际上跟使用let 关键字顺序声明变量一样:

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

这里的默认参数会按照定义它们的顺序依次被初始化。可以依照如下示例想象一下这个过程:

function makeKing() {
  let name = 'Henry';
  let numerals = 'VIII';
  return `King ${name} ${numerals}`;
}

因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。看下面这个例子:

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

参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛出错误:

// 调用时不传第一个参数会报错
function makeKing(name = numerals, numerals = 'VIII') {
  return `King ${name} ${numerals}`;
}

参数也存在于自己的作用域中,它们不能引用函数体的作用域:

// 调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals = defaultNumeral) {
  let defaultNumeral = 'VIII';
  return `King ${name} ${numerals}`;
}

7.扩展参数

当我们希望把数组参数里面每个元素挨个传入某个函数:
可以这样:

console.log(getSum.apply(null, values)); // 10

ES6中可以这样:

console.log(getSum(...values)); // 10

这样就可以吧values展开

对于arguments对象来说,这些是不知道的,它只知道外面是挨个传参进来的:

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

8.收集参数

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);
}
ignoreFirst();       // []
ignoreFirst(1);      // []
ignoreFirst(1,2);    // [2]
ignoreFirst(1,2,3);  // [2, 3]

箭头函数虽然不支持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));

当然,改变values不改变arguments

9.函数声明与函数表达式的区别

函数声明存在声明提升,也就是说,函数的声明会提升到代码的顶部,优先级比变量高,所以函数声明出来的函数可以提前使用,而函数表达式不能。

10.函数作为值

函数名就是一个指针变量,所以函数也可以当成参数传递。

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"

作用实例:
通过判断参数名来决定比较哪一个属性。

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

.
11.函数内部

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

arguments

它是传入的参数的类数组对象,有一个callee属性,有什么用呢,看看这个例子:

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

这个是通过递归求阶乘,在这里,内部是紧密耦合的,也就是说假如我们把factorial的函数名称改了,这个函数就失效了,再来看看如何改变:

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}

通过arguments.callee属性,它指向的是arguments对象所在的函数,也就是说,相当于调用本函数。

这样就可以解耦了,就算外部名称变了,函数依然有用。

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'

在箭头函数中,this 引用的是定义箭头函数的上下文,也就是定义该函数时,作用域指向的对象

window.color = 'red';
let o = {
  color: 'blue'
};
let sayColor = () => console.log(this.color);
//定义时所在作用域是Window指向就是Window
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

caller

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

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);

}
outer();

上面代码会打印outer的源码

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

function outer() {
  inner();
}
function inner() {
  console.log(arguments.callee.caller);
}
outer();

在严格模式下访问arguments.callee 会报错。
ECMAScript 5也定义了arguments.caller ,但在严格模式下访问它会报错,在非严格模式下则始终是undefined 。这是为了分清arguments.caller 和函数的caller 而故意为之的。
而作为对这门语言的安全防护,这些改动也让第三方代码无法检测同一上下文中运行的其他代码。
严格模式下还有一个限制,就是不能给函数的caller 属性赋值,否则会导致错误。

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"

12.函数属性与方法
length 属性保存函数定义的命名参数的个数

prototype 属性保存原型对象的指针,原型对象保存了实例应该有的属性和方法

apply() 和call() 。这两个方法都会以指定的this 值来调用函数,即会设置调用函数时函数体内this 对象的值。apply() 方法接收两个参数:函数内this 的值和一个参数数组。第二个参数可以是Array 的实例,但也可以是arguments 对象。

function sum(num1, num2) {
  return num1 + num2;
}
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

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

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()
返回函数本身。

13.函数表达式

let functionName = function(arg0, arg1, arg2) {
  // 函数体
};

函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName 。这样创建的函数叫作匿名函数(anonymous funtion),因为function 关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数 )。未赋值给其他变量的匿名函数的name 属性是空字符串。

函数表达式跟JavaScript中的其他表达式一样,需要先赋值再使用。

理解函数声明与函数表达式之间的区别,关键是理解提升。比如,以下代码的执行结果可能会出乎意料:

// 千万别这样做!
if (condition) {

  function sayHi() {
    console.log('Hi!');
  }
} else {
  function sayHi() {
    console.log('Yo!');
  }
}

这段代码看起来很正常,就是如果condition 为true ,则使用第一个sayHi() 定义;否则,就使用第二个。事实上,这种写法在ECAMScript中不是有效的语法。JavaScript引擎会尝试将其纠正为适当的声明。问题在于浏览器纠正这个问题的方式并不一致。多数浏览器会忽略condition 直接返回第二个声明。Firefox会在condition 为true时返回第一个声明。这种写法很危险,不要使用。不过,如果把上面的函数声明换成函数表达式就没问题了:

// 没问题
let sayHi;
if (condition) {
  sayHi = function() {
    console.log("Hi!");
  };
} else {
  sayHi = function() {
    console.log("Yo!");
  };
}

14.关于递归
上面的阶乘的例子,假如我们在严格模式下不能使用arguments.callee,那么可以使用命名函数表达式(named functionexpression)达到目的。比如:

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

这里创建了一个命名函数表达式f() ,然后将它赋值给了变量factorial 。即使把函数赋值给另一个变量,函数表达式的名称f 也不变,因此递归调用不会有问题。这个模式在严格模式和非严格模式下都
可以使用。

15.尾调用优化

ECMAScript 6规范新增了一项内存管理优化机制,让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尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。

条件:

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

不符合的例子:

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

尾调用优化不在乎是不是递归,只在乎满不满足条件。只不过在递归下很明显可以优化栈帧的数量。

之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用f.arguments 和f.caller ,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此尾调用优化要求必须在严格模式下有效,以防止引用这些属性。

尾调用优化的代码实例:

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

function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}
console.log(fib(0));  // 0
console.log(fib(1));  // 1
console.log(fib(2));  // 1
console.log(fib(3));  // 2
console.log(fib(4));  // 3
console.log(fib(5));  // 5
console.log(fib(6));  // 8

显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。结果,fib(n) 的栈帧数的内存复杂度是 O2^n。因此,即使这么一个简单的调用也可以给浏览器带来麻烦

不过,也可以保持递归实现,但将其重构为满足优化条件的形式。为此可以使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归:

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

//妙啊~

这样重构之后,就可以满足尾调用优化的所有条件,再调用fib(1000)就不会对浏览器造成威胁了。

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

如前面用过的例子:

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

这里返回的函数里用到了原函数里面的参数

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

看看例子:

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]] 中。

在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]] 来创建其作用域链。

接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。

函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。不过,闭包就不一样了。

let compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });


在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。因此,在createComparisonFunction() 函数中,匿名函数的作用域链中实际上包createComparisonFunction() 的活动对象。

这里返回的匿名函数就会保留对前一个函数对象中的活动对象的引用,所以可以访问到前一个函数对象里面的变量。

这样,匿名函数就可以访问到createComparisonFunction()可以访问的所有变量。

另一个有意思的副作用就是,createComparisonFunction() 的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在createComparisonFunction() 执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销
毁后才会被销毁:

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

因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,因此建议仅在十分必要时使用

总结一下,理解闭包的关键在于理解:

变量对象,活动对象,函数执行时会把当前的上下文添加到作用域链中

17.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()返回了一个匿名函数,然后执行这个匿名函数,所以是window执行的方法,也就是说这里的this,不是外部函数的this,是内部函数的this。

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

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

this 和arguments 都是不能直接在内部函数中访问的。如果想访问包含作用域中的arguments 对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。

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

第一个调用函数的对象就是object,所以没问题
第二个前面括号和没有括号是等价的,所以也没问题
第三个赋值返回了函数本身,然后运行,所在window环境里运行的

18.内存泄漏

闭包可能导致的内存泄漏:

function assignHandler() {
  let element = document.getElementById('someElement');
  element.onclick = () => console.log(element.id);
}

这里element始终被onclick引用,所以就不会被释放。

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对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。

19.立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式 (IIFE,Immediately Invoked Function Expression)

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

在ES5中可以用它模拟块级作用域:

// IIFE
(function () {
  for (var i = 0; i < count; i++) {
    console.log(i);
  }
})();
console.log(i);  // 抛出错误

ES6中没必要这么麻烦:

// 内嵌块级作用域
{
  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); // 抛出错误

20.私有变量

严格来讲,JavaScript没有私有成员的概念,所有对象属性都公有的。
不过,倒是有私有变量 的概念。

function add(num1, num2) {
  let sum = num1 + num2;
  return sum;
}

在这个函数中,函数add() 有3个私有变量:num1 、num2 和sum 。

我们可以创建某些方法,来专门访问这些私有变量,就叫:
特权方法 (privileged method)是能够访问函数私有变量(及私有函数)的公有方法。

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'

构造函数模式的缺点是每个实例都会重新创建一遍新方法。使用静态私有变量实现特权方法可以避免这个问题。

function Person(name) {
			function sayHello() {
				console.log("hello");
			};

			this.sayHello = sayHello();
		}
		let person1 = new Person('Nicholas');
		let person2 = new Person('ZXL');
		console.log(person1.sayHello === person2.sayHello)  //true

21.静态私有变量

特权方法也可以通过使用私有作用域定义私有变量和函数来实现。这个模式如下所示:

(function() {
  // 私有变量和私有函数
  let privateVariable = 10;
  function privateFunction() {
    return false;
  }
  // 构造函数
  MyObject = function() {};
  // 公有和特权方法
  MyObject.prototype.publicMethod = function() {
    privateVariable++;
    return privateFunction();
  };
})();

这个模式下,引用保存在原型里面,所有实例的原型都是共享的。

(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'

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

22.模块模式

单例对象(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);
      }
    }
  };
}();

在Web开发中,经常需要使用单例对象管理应用程序级的信息。上面这个简单的例子创建了一个application 对象用于管理组件。在创建这个对象之后,内部就会创建一个私有的数组components ,然后将一个BaseComponent 组件的新实例添加到数组中。(BaseComponent 组件的代码并不重要,在这里用它只是为了说明模块模式的用法。)对象字面量中定义的getComponentCount() 和registerComponent() 方法都是可以访问components 私有数组的特权方法。前一个方法返回注册组件的数量,后一个方法负责注册新组件。

23.模块增强模式:

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

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

与模块模式的区别就是,这里返回的是一个组件实例,而模块模式返回的是接口方法的集合。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值