7. 函数

函数定义

/*
    函数实际上是对象。
    每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。
    函数是对象,函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定
*/



// 函数声明式
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");  // 不推荐
// 这段代码会被解释两次
//   - 第一次是把它当作常规 ECMAScript 代码
//   - 第二次是解释传给构造函数的字符串(会影响性能)
//   - 把函数想象为对象,把函数名想象为指针是很重要的

箭头函数

很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建对象行为是相同的
任何可以使用函数表达式的地方,都可以使用箭头函数

let arrowSum = (a, b) => {
	return a + b;
};

let functionExpression = function(a, b) {
	return a + b;
};

console.log(arrowSum(5, 8));
console.log(functionExpression(5, 8));

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

let ints = [1, 2, 3];

console.log(ints.map(function (i) {return i + 1}));
console.log(ints.map((i) => {return i + 1}));

如果只有一个参数,那也可以不用括号
没有参数,或者多个参数的情况下,才需要使用括号

// 以下两种写法都有效
let double = (x) => {return 2 * x;};
let triple = x => {return 3 * x;};

// 没有参数需要括号
let getRandom = () => {return Math.random();};

// 多个参数需要括号
let sum = (a, b) => {return a + b;};

// 无效的写法
let multiply = a, b => {return a * b;};

箭头函数可以不用大括号,但这样会改变函数的行为
使用大括号说明包含 "函数体”,可以在一个函数中包含多条语句,跟常规的函数一样
如果不使用大括号,那么箭头函数后面就只能有一行代码(赋值操作,一个表达式)
省略大括号会隐式返回这行代码的值

// 一下两种写法都有效,而且返回相应的值
let double = (x) => {return x * 2}
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;

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

函数名

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

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

console.log(sum(10, 10));

let anotherSum = sum;
console.log(anotherSum(10, 10));

sum = null;
console.log(anotherSum(10, 10));

ECMAScript6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息
多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名
即使函数没有名称,也会如实显示成空字符串
如果它是 Fcuntion 构造函数创建的,会标识成 “anonymous”

function foo(){};
let bar = function(){};
let baz = () => {};

console.log(foo.name);
console.log(bar.name);
console.log(baz.name);
console.log((() => {}).name);
console.log((new Function()).name);

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

function foo(){};
console.log(foo.bind(null).name);

let dog = {
    years:1,
    get age(){
        return this.years;
    },
    set age(value){
        this.years = value;
    }
}

let propertyDesciptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDesciptor.get.name);
console.log(propertyDesciptor.set.name)

理解参数

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

ECMAScript 函数参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。

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

arguments 对象是一个类数组对象(但不是Array的实例)
  - 可以使用中括号语法访问其中的元素(第一个参数是 arguments[0], 第二个参数是 arguments[1])
  - 要确定传进来多少个参数,可以访问 arguments.length 属性
function sayHi(name, message){
    // sayHi 的第一个参数叫 name, 第二个参数叫 message
    console.log("Hello " + name + ", " + message);

    // name 对应 arguments[0],  message 对应 arguments[1],
    console.log("Hello " + arguments[0] + ", " + arguments[1]);
}

sayHi("GuoKeT", "Welcome new World")

重写

function howManyArgs(){
    console.log(arguments.length);
}


howManyArgs("string", 45);
howManyArgs();
howManyArgs(12);

ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的

与其他语言不同,在 ECMAScript 中的命名参数不会创建让之后的调用必须匹配的函数签名。因为根本不存在验证命名参数的机制

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

箭头函数中的参数

如果函数是使用箭头语法定义的,传给函数的参数将不能使用 arguments 关键字访问(只能通过定义的命名参数访问)
function foo(){
    console.log(arguments[0]);
}

foo(5);

let bar = () => {console.log(arguments[0]);};
bar(5)  // ReferenceError: arguments is not defined

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

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

foo(5);

没有重载

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

如果在 ECMAScript 中定义了两个同名函数,后定义的会覆盖先定义的

function addSomeNumber(num) {
    return num + 100;
}

function addSomeNumber(num) {
    return num + 200;
}

let result = addSomeNumber(100);

console.log(result);

第二个定义会覆盖第一个定义

默认参数值

ES5 及之前,实现默认参数的一种常用方式就是检测某个参数是否等于 undefined,

function makeKing(name){
    name = (typeof name !== undefined) ? name : "Henry";
    return `King ${name} VIII`   
}

console.log(makeKing());
console.log(makeKing('Louis'));

ES6 之后支持显式定义默认参数

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


console.log(makeKing())
console.log(makeKing('Louis'))
console.log(makeKing(undefined, "VII"))

给参数传 undefined 相当于没有传值
使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数
修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准

function makeKing(name="Henry"){
    name = "Louis";
    return `King ${arguments[0]}`;
}



console.log(makeKing());
console.log(makeKing("Louis"));

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

let romanNumerals = ["I", "II", "III", "IV"];

let ordinality = 0;


function getNumerals(){
    // 每次调用后递增
    return romanNumerals[ordinality++];
}


function makeKing(name="Henry", numerals = getNumerals()){
    return `King ${name} ${numerals}`;
}

console.log(makeKing());
console.log(makeKing('Louis', 'XVI'));
console.log(makeKing());
console.log(makeKing());

// 函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值
// 计算默认值的函数只有在调用函数但未传相应参数时才会被调用

// 箭头函数同样也可以这样使用默认参数,只不过在只有一个参数时,就必须使用括号尔不能省略了
let makeKing1 = (name="Henry") => `King ${name}`;
console.log(makeKing1());

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

在求值默认参数时可以定义对象,也可以动态调用函数(函数参数肯定是在某个作用域中求值的)

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

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


console.log(makeKing())

// 默认参数会按照定义它们的顺序依次被初始化。
function makeKing1(){
    let name = "Henry";
    let numerals = "VIII";
    return `King ${name} ${numerals}`;
}

参数按顺序初始化的,后定义默认值的参数可以引用先定义的参数

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

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

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

function makeKIng(name="Henry", numerals=defaultNumeral){
    let defaultNumeral="VIII";
    return `King ${name} ${numerals}`;
}

参数扩展与收集

扩展参数

给函数传参时,有时候可能不需要传一个数组,而是要传入数组的元素

let values = [1, 2, 3, 4];

function getSum(){
    let sum = 0;
    for (let i = 0; i < arguments.length; ++i){
        sum += arguments[i];
    }
    return sum;
}

// 如果不使用扩展操作符,想把定义在这个函数这面的数组拆分,就得求助于 apply() 方法
console.log(getSum.apply(null, values));

// ES6中,可以通过扩展操作符极为简洁地实现这种操作
// 对可迭代对象应用扩展操作符,并将作为一个参数传入,可以将可迭代对象拆分,并将迭代返回地每个值单独传入
console.log(getSum(...values));

// 数组长度已知,在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值
console.log(getSum(-1, ...values));
console.log(getSum(...values, 5));
console.log(getSum(-1, ...values, 5));
console.log(getSum(...values, ...[5, 6, 7]))

arguments 对象只是消费扩展操作符的一种方式

在普通函数和箭头函数中,也可以将扩展操作符用于命名参数

function getProduct(a, b, c = 1){
    return a * b * c;
}

let getSum = (a, b, c = 0) => {
    return  a + b + c;
}


console.log(getProduct(...[1, 2]));
console.log(getProduct(...[1, 2, 3]));
console.log(getProduct(...[1, 2, 3, 4]));

console.log(getSum(...[1, 2]))
console.log(getSum(...[1, 2]));
console.log(getSum(...[1, 2, 3]));

参数收集

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组
(类似 arguments 对象的构造机制,收集参数的结果会得到一个 Array 实例)
function getSum(...values) {
    // 程序累加 values 中的所有值
    // 初始值的总和为 0
    return values.reduce((x, y) => x + y, 0);
}

console.log(getSum(1, 2, 3));
console.log(getSum())

收集参数的前面如果还有命名参数,则只会收集其余的参数

如果没有则会得到空数组,因为收集的参数结果可变,所以只能把它作为最后一个参数

// 不可以
// function getProduce(...values, lastValue){}

// 可以
function ignorFirst(firstValue, ...values) {
    console.log(values);
}

ignorFirst();
ignorFirst(1);
ignorFirst(1, 2);
ignorFirst(1, 2, 3);

箭头函数虽然不支持 arguments 对象,但支持收集参数的定义方式,因此也可以实现与使用 arguments 一样的逻辑

let getSum = (...values) => {
    return values.reduce((x, y) => x + y, 0)
};

console.log(getSum(1, 2, 3));

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

function getSum(...values) {
    console.log(arguments.length);
    console.log(arguments);
    console.log(values);
}

getSum(1, 2, 3)

函数声明与函数表达式

JS 引擎在任何代码执行之前,会先读取 函数声明,并在执行上下文中生成函数定义
函数表达式 必须等到代码执行到它那一行,才会在执行上下文中生成函数定义

函数声明提升(即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部)

// 没问题
console.log(sum(10, 10))
function sum(num1, num2){
    return num1 + num2;
}

函数表达式并不会提升

在使用函数表达式初始化变量时,也可以给函数一个名称,比如 let sum = function sum(){}

函数作为值

函数名在 ECMAScript 中就是变量,函数可以用在任何可以使用变量的地方
不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数

// 这个函数可以接收两个参数,
//   - 第一个参数应该是一个函数,
//   - 第二个参数应该是传给这个函数的值
function callSomeFunction(someFunction, somArgument) {
    return someFunction(somArgument);
}

// 任何函数都可以想下面这样作为参数传递
function add(num) {
    return num + 10;
}
let result1 = callSomeFunction(add, 10);
console.log(result1)


function getCreate(name) {
    return "Hello" + name;
}
let result2 = callSomeFunction(getCreate, "Nicholas");
console.log(result2);

函数内部

arguments

arguments 是一个类数组对象,包含调用函数时传入的所有参数
只有以 function 关键字定义函数时才会有

arguments 对象还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针

function factorial(num) {
    if(num <=1 ) {
        return 1;
    }else{
        return num * factorial(num - 1);
    }
}
// 阶乘函数一般定义成递归调用的,只要给函数一个名称,且这个名称不会变,定义就没有问题
// 这个函数要正确执行就必须保证函数名是: factorial,从而导致了精密耦合

使用 argument.callee 就可以让函数逻辑与函数名解耦

// 使用 argument.callee 就可以让函数逻辑与函数名解耦
function factorial(num) {
    if (num < 1){
        return 1;
    }else{
        return num * arguments.callee(num - 1);
    }
}

// 重写之后 factorial() 函数以及用 arguments.callee 代替了之前的硬编码 factoral
// 无论函数叫什么名称,都可以引用正确的函数

let trueFactorial = factorial;
factorial = function() {
    return 0;
}

console.log(trueFactorial(5));
console.log(factorial(5));

this

标准函数

标准函数
  - this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值
  - 在全局网页的全局上下文中调用函数时,this 指向 window
window.color = 'red';

let o = {
    color: "blue"
}

function sayColor(){
    console.log(this.color);
}

// this 到底引用哪个对象必须到函数被调用时才能确定。
// 这个值在代码执行的过程中可能会变
sayColor();
o.sayColor = sayColor;
o.sayColor();

箭头函数

箭头函数
  - this 引用的是定义箭头函数的上下文
 window.color = 'red';

let o = {
    color: "blue"
}

let sayColor = () => console.log(this.color);


// 箭头函数是在 window 上下文中定义的
sayColor();
o.sayColor = sayColor;
o.sayColor();

// 函数名只是保存指针的变量
// 全局定义的 sayColor() 函数和 o.sayColor() 是同一个函数,只不过执行的上下文不同

事件回调或定时回调中调用某个函数时,this 指向的并非想要的对象
将回调函数携程箭头函数就可以解决问题
箭头函数中的 this 会保留定义该函数时的上下文

function King() {
    this.royaltyName = "Henry";
    // this 引用 King 的实例
    setTimeout(() => console.log(this.royaltyName), 100);
}

function Queen() {
    this.royaltyName = "Elizabeth";

    // this 引用 Window 对象
    setTimeout(function(){ console.log(this.royaltyName); }, 1000);
}

new King();
new Queen();

caller

caller 这个属性引用的是调用当前函数的函数
或者如果是在全局作用域中调用则为null
function outer() {
    inner();
}

function inner() {
    console.log(inner.caller);  // 引用的是调用当前函数的函数
}

outer();
// outer() 调用了 inner()
// inner.caller 指向 outer()

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

function outer() {
    inner();
}

function inner() {
    // callee 属性,是一个指向 arguments 对象所在函数的指针
    console.log(arguments.callee.caller);
}

outer();
// outer() 调用了 inner()
// inner.caller 指向 outer()

严格模式下访问 aguments.callee 会报错
严格模式下不能给函数的 caller 属性赋值,否则会导致错误

new.tatget

···
ES6 新增了检测函数是否使用 new 关键字调用 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();  // Uncaught Error King must be instantiated using 'new'
King();  // King instantiated using 'new'

函数属性与方法

length

length 属性保存函数定义的命名参数个数

function sayName(name) {
    console.log(name);
}

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

function sayHi(){
    console.log("Hi");
}

console.log(sayName.length);
console.log(sum.length);
console.log(sayHi.length);

prototype

prototype 是保存引用类型所有实例方法的地方
  - toString(), valueOf() 等方法实际上都保存在 prototype 上,由所有实例共享
  - 这个属性在自定义类型时特别重要

ES5中 prototype 属性是不可枚举的,使用 for-in 循环不会返回这个属性

apply(), call(), bind()

这两个方法会以指定的 this 值来调用函数,即会设置调用函数时函数体内 this 对象的值

apply()

apply() 方法接收两个参数
  - 函数内 this 的值
  - 参数数组(可以是 Array 实例,也可以是 arguments 对象)
function sum(num1, num2){
    return num1 + num2;
}

function callSum1(num1, num2){
    return sum.apply(this, arguments);  // 传入 arguments 对象
}
console.log(callSum1(10, 10))

//

function callSum1(num1, num2){
    return sum.apply(this, [num1, num2]);  // 传入 Array 数组
}
console.log(callSum1(10, 10))

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

call()

call() 方法接收两个参数
  - 函数内 this 的值
  - 参数是逐个传递的
function sum(num1, num2){
    return num1 + num2;
}

function callSum1(num1, num2){
    return sum.call(this, num1, num2);  // 传入 arguments 对象
}
console.log(callSum1(10, 10))

apply(), call() 总结

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

window.color = 'red';
let o = {
    color: 'blue'
}

function sayColor() {
    console.log(this.color);
}

sayColor();
sayColor.call(this);  // 指向 window
sayColor.call(window);  // 指向 window
sayColor.call(o); // 指向 o

bind()

bind() 方法会创建一个新的函数实例,其 this 值会被绑定到传给 bind() 的对象
window.color = 'red';
let o = {
    color: 'blue'
}

function sayColor() {
    console.log(this.color);
}

let objectSayColor = sayColor.bind(o);
objectSayColor();  // blue

函数表达式

函数声明是这样的

// 函数声明的关键特点是 
//   - 函数声明提升,会在代码执行之前获得定义(意味着函数声明可以出现在调用它的代码之后)
sayHi()
function sayHi(){
	console.log('Hi! ')
}

函数表达式

// 函数表达式有几种不同的形式,最常见的
let functionName = function(arg0, arg1, arg2){
    // 函数体
};
函数表达式看起来像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量 functionName
  - 这样创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符
  - 未赋值给其他变量的匿名函数 name 属性是空字符串

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

函数声明与函数表达式之间的区别,关键是理解提升

递归

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

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

// 这是经典的递归阶乘函数。
// 虽然这样写是可以的,但是如果把这个函数赋值给其他变量,就会出现问题
let anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(4));  // 报错

在写递归时,使用 arguments.callee 可以避免这个问题

function factorial(num) {
    if (num <= 1) {
        return 1;
    }else {
        // arguments.callee 就是一个指向正在执行的函数的指针
        // 可以确保无论通过什么变量调用这个函数都不会出问题
        // 编写函数时,arguments.callee 时引用当前函数的首选
        // 严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错(可以使用命名函数表达式达到目的)
        return num * arguments.callee(num - 1);
    }
}

let anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(4));

尾调用优化

尾调用:即外部函数的返回值是一个内部函数的返回值

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

尾调用优化的条件

尾调用优化的条件就是确定外部栈帧真的没有必要存在了
  - 代码在严格模式下执行
  - 外部函数的返回值是对尾调用函数的调用
  - 尾调用函数返回后不需要执行额外的逻辑
  - 尾调用函数不是引用外部函数作用域中自由变量的闭包

不符合尾调用优化的要求

"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 innerFcuntion(a, b) {
    return innerFunction(a + b);
}

// 有优化:初始返回值不涉及栈帧
function outerFuntion(a, b) {
    if (a < b){
        return a;
    }
    return innerFunction(a + b);
}


// 有优化:两个内部函数都在尾部
function outerFunction(condition) {
    return condition ? innerFunctionA() : innerFunctionB();
}

差异化尾调用和递归调用是容易让人混淆的地方
无论是递归调用还是尾调用,都可以应用优化
引擎并不区分尾调用中的是函数自身还是其他函数
这个优化在递归场景下的效果是最明显的,因为递归代码最容易在栈内存中迅速产生大量堆栈

非严格模式下函数调用中允许使用 f.arguments 和 f.callee, 而它们都会引用外部函数的栈帧
尾调用优化要求必须在严格模式有效,以防止引用这些属性

尾调用的优化代码

可以通过把简单的递归函数转换为待优化的代码来加深对调用优化的理解

function fib(n) {
    if (n < 2) {
        return n;
    }
    return fib(n -1) + fib(n - 2);
}

let stratrTime = new Date().getTime();
console.log(fib(1000));
console.log(new Date().getTime() - stratrTime)
console.log('====================')
// 这个函数很显然不符合尾调用优化的条件,因为返回语句中有一个相加的操作
//   - 把递归写成迭代循环形式


// 保持递归实现,将其重构为满足优化条件的形式。

"use strict";
// 基础框架
function fib(n) {
    return fibTmp1(0, 1, n);
}

// 执行递归
function fibTmp1(a, b, n) {
    if (n === 0) {
        return a;
    }
    return fibTmp1(b, a + b, n - 1);
}
// 这样重构之后,就可以猫族尾调用优化的所有条件,在调用 fib(1000) 就不会对浏览器造成威胁了
stratrTime = new Date().getTime();
console.log(fib(1000));
console.log(new Date().getTime() - stratrTime)

闭包

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

function createComparisonFunction(propertyName) {
    return function(object1, object2) {
    	// 这个函数内部被返回并在其他地方被使用后,它仍然引用着那个变量
    	// 因为函数作用域链包含 createComparisonFunction() 函数的作用域
        let value1 = object1[propertyName];  // 引用了外部变量 propertyName
        let value2 = object2[propertyName];  // 引用了外部变量 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 时,会为它创建一个包含 arguments, value1, value2 的活动对象(这个对象时其作用域链上的第一个对象)
// 全局上下文的变量对象则是 compare() 作用域链上的第二个对象,包含 this, result 和 compare

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

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"
// 每个函数在被调用时都会自动创建两个特殊变量:this 和 arguments
// 内部函数永远不可能直接访问外部函数的这两个变量。

// 如果把 this 保存在闭包可以访问的另一个变量中,则是行得通的
object = {
    identity: "My Object",
    getIdentityFunc() {
        let that = this;
        return function() {
            return that.identity;
        }
    }
}
console.log(object.getIdentityFunc()()); // "My Object"

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

一些特殊情况下,this的值可能并不是所期待的值

window.identity = "The Window";


let object = {
    identity: "My Object",
    getIdentityFunc() {
        return this.identity;
    }
}

// 正常调用
console.log(object.getIdentityFunc());  // My Object


// 虽然加了括号之后看起来是对一个函数的引用,但 this 值并没有变
// 按照规范,object.getIdentity 和 (object.getIdentity) 是相等的
console.log((object.getIdentityFunc()));  // My Object


// 执行了一次赋值,然后再调用赋值后的结果
// 因为赋值表达式的值是函数本身,this值不再与任何对象绑定,所以返回的是 "The Window"
console.log((object.getIdentityFunc = object.getIdentityFunc)())  // The Window

// 一般情况下不大可能像第二行和第三行这样调用对象上的方法
// 但可以知道,即使语法稍有不同,也可能影响 this 的值

立即调用的函数表达式

立即调用的你们函数又被称作: 立即调用的函数表达式
它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式

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

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

// ES5 之前, IIFE 相当普遍
// IIFE
(function() {
    for (var i = 0; i < CountQueuingStrategy; i++){
        console.log(i)
    }
})();

console.log(i); // 抛出错误
// ES6 之后,IIFE就没有那么必要了,因为块级作用域中的变量无需 IIFE 就可以实现同样的隔离

私有变量

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

私有变量包括
  - 函数参数
  - 局部变量
  - 函数内部定义的其他函数
function add(num1, num2){
    let sum = num1 + num2;
    // num1, num2, sum (三个私有变量)
    // 只能在函数内部访问
    return sum;
}

特权方法(privileged method)

特权方法是能够访问函数私有变量(及私有函数)的公有方法
对象上有两种方式创建特权方法

构造函数中实现

// 这个模式是把所有私有变量和私有函数都定义在构造函数中
function MyObject() {
    // 私有变量和私有函数
    let privateVariable = 10;

    function privateFunction(){
        return false;
    }

    // 特权方法
    // 创建一个能够访问这些私有成员的方法(特权方法)
    // 定义在构造函数中的特权方法其实是一个闭包
    // 它具有访问构造函数中定义的所有变量和函数的能力
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    }
}
function Person(name) {
    this.getName = function() {
        return name;    
    };

    this.setName = function(value) {
        name = value;
    };
}

let person = new Person("Nicholas");
console.log(person.getName())
person.setName("Greg");
console.log(person.getName());

// 代码中构造函数定义了两个特权方法
// getName() 和 setName()
// 每个方法都可以构造函数外部调用,并通过它们来读写私有的 name 变量
// Person 构造函数外部,没有别的办法访问 name
// 私有变量 name 对每个 Person 实例而言都是独一无二的,每次调用构造函数都会重新创建一套变量和方法

必须通过构造函数来实现这种隔离,构造函数模式的缺点是每个实例都会创建一遍新方法
使用静态私有变量实现特权方法可以避免这个问题

静态私有变量

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

(function() {
    // 私有变量和函数
    let privateVariable = 10;

    function privatFunction() {
        return false;
    };

    // 构造函数
    MyObject = function() {};

    // 公有和特权方法
    MyObject.prototype.publicMethod = function() {
        privateVariable++;
        return privatFunction;
    }
})
// 匿名函数表达式创建了一个包含构造函数及其方法的私有作用域

// 首先定义的是私有变量和私有函数,然后又定义了构造函数和公有方法
//   - 公有方法定义在构造函数的原型上,与典型的原型模式一样
//   - 这个模式定义的构造函数没有使用函数声明,使用的是函数表达式
//   - 函数声明会创建内部函数,在这里并不是必须的

// 声明 MyObject 并没有使用任何关键字
//   - 不使用关键字声明的变量会创建在全局作用域中,MyObject 是全局变量
//   - Myobject 可以在私有作用域外部被访问
//   - 严格模式下给未声明的变量赋值会导致错误

这个模式与前一个模式的主要区别就是,私有变量和私有函数是由实例共享的
特权方法定义在原型上,所以同样是由实例共享的
特权方法作为一个闭包,使用引用着包含它的作用域

(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());
person1.setName("Matt");
console.log(person1.getName());

let person2 = new Person("Michel");
console.log(person1.getName());
console.log(person2.getName());
// Person 构造函数可以访问私有变量 name
// 跟 getName() 和 setName() 方法一样
// 使用这种模式,name 变成了静态变量,可供所有实例使用

// 意味着在任何实力上调用 setName() 修改这个变量都会影响其他实例
// 调用 setName() 或创建新的 Person 实例都要把 name 变量设置未一个新值
// 而所有实例都会返回相同的值

// 这样创建的静态私有变量可以利用原型更好的重用代码,只是每个实例没有了自己的私有变量
// 最终,到底还是把私有变量放在实例中,还是作为静态私有变量,都需要根据自己的需求来确定

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

模块模式

模块模式,在一个单例对象上实现了相同的隔离和封装
单例对象(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 singleton = function() {
    // 私有变量和私有函数
    let privateVariable = 10;

    function privateFunction() {
        return false;
    }

    // 创建对象
    let object = new CustomType();

    // 添加特权/公有属性和方法
    object.publicProperty = true;

    object.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    }

    // 返回对象
    return object;
}
  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值