IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> JavaScript高级程序设计第十章--- 函数(二) -> 正文阅读

[JavaScript知识库]JavaScript高级程序设计第十章--- 函数(二)

作者:recommend-item-box type_blog clearfix

第十章(二)

本章内容:

  • 使用函数实现递归
  • 使用闭包实现私有变量

10.11 函数表达式

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

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

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

// 千万别这样做!
if (condition) {
	function sayHi() {
		console.log('Hi!');
	}
} else {
	function sayHi() {
		console.log('Yo!');
	}
}

如果把上面的函数声明换成函数表达式就没问题了:

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


10.12 递归

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

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 {
		return num * arguments.callee(num - 1);
	}
}

不过,在严格模式下运行的代码是不能访问arguments.callee 的,因为访问会出错。此时,可以使用命名函数表达式(named function expression)达到目的。



10.13 尾调用优化

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

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

即外部函数的返回值是一个内部函数的返回值时,因为返回值相同所以可以直接将outer函数弹出栈外,入栈inner函数进行计算返回后弹出。

10.13.1 尾调用优化条件

P307

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

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

10.13.2 尾调用优化的代码

可以通过把简单的递归函数转换为待优化的代码来加深对尾调用优化的理解。下面是一个通过递归计算斐波纳契数列的函数:

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

显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。

解决这个问题也有不同的策略,比如把递归改写成迭代循环形式。不过,也可以保持递归实现,但将其重构为满足优化条件的形式。为此可以使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归:

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

这样重构之后,就可以满足尾调用优化的所有条件,多次调用也不会对浏览器产生威胁。

写成迭代:

function loopFib(n) {
    let a = 0, b = 1, tmp;
    for (let i=0; i<n; i++){
        tmp = b;
        b = a + b;
        a = tmp;
    }
    return a;
} 


10.14 闭包

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

关于闭包的分析及例题
深入贯彻闭包思想,全面理解JS闭包形成过程

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


function compare(value1, value2) {
	if (value1 < value2) {
		return -1;
	} else if (value1 > value2) {
		return 1;
	} else {
		return 0;
	}
}

let result = compare(5, 10);

对于上面这个例子,
定义函数时,会创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。
在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。在这个例子中,这意味着compare()函数执行上下文的作用域链中有两个变量对象:局部变量对象和全局变量对象。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

结构如下图所示:
在这里插入图片描述

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。


当使用闭包时,

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 compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });

上面这个代码执行完后,compare会被赋值为createComparisonFunction中的一个内部函数,执行后会具有下面这种结构:

在这里插入图片描述
即compare作为一个内部函数,作用域链中包含父函数createComparisonFunction的活动对象,这样就可以访问父函数能访问到的所有变量。

同时,由于这个匿名函数compare会一直拥有对父函数活动对象的引用,所以createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁:

// 解除对函数的引用,这样就可以释放内存了
compare = null;

把compareNames 设置为等于null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放掉。


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

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

分析:
这里先创建了一个全局变量identity,之后又创建一个包含identity 属性的对象。这个对象还包含一个getIdentityFunc()方法,返回一个匿名函数。这个匿名函数返回this.identity。因为getIdentityFunc()返回函数,所以object.getIdentityFunc()()会立即调用这个返回的函数,从而得到一个字符串。

这里至于为什么返回的是window对象的值?

每个函数在被调用时都会自动创建两个特殊变量:this 和arguments。内部函数永
远不可能直接访问外部函数的这两个变量。
内部函数需要根据作用域链的顺序,优先访问自身的值。this指向把函数当成方法调用的上下文对象,此时调用的上下文对象是window,所以会返回window对象的值。

换一种写法:

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 保存
到变量that 中。然后在闭包中,返回that对应的值。

因为getIdentityFunc()的上下文对象是object,所以会将object对象保存到变量that中,这样在内部函数中调用时,会从作用域链找到这个that的变量并且没有名称冲突,所以会返回object的对应属性。

注意
this 和arguments 都是不能直接在内部函数中访问的。如果想访问包含作用域中的arguments 对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。(我觉得是因为每个函数自己都会有这两个对象,因此会优先访问自己的)

10.14.2 内存泄漏

闭包产生的引用会导致垃圾处理程序无法回收(引用计数)。

要赋值null才能彻底保证空间回收。



10.15 立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked FunctionExpression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。下面是一个简单的例子:

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

第一个括号内部会被看做一个函数表达式,

(function() {})

因此最后面的第二个括号会被看作执行这个函数,函数会立即执行。

(function (){})()

使用IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。
// IIFE
(function () {
	for (var i = 0; i < count; i++) {
		console.log(i);
	}
})();

console.log(i); // 抛出错误

前面的代码在执行到IIFE 外部的console.log()时会出错,因为它访问的变量是在IIFE 内部定义的,在外部访问不到。在ECMAScript 5.1 及以前,为了防止变量定义外泄,IIFE 是个非常有效的方式。这样也不会导致闭包相关的内存问题,因为不存在对这个匿名函数的引用。为此,只要函数执行完毕,其作用域链就可以被销毁。

在ECMAScript 6 以后有了块级作用域,IIFE 就没有那么必要了。



10.16 私有变量

任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。即一般方法不能直接访问,但通过特殊方法可以访问的变量。

通过闭包可以创建出能够访问私有变量的公有方法,即特权方法。

特权方法是能够访问函数私有变量(及私有函数)的公有方法。在对象上有两种方式创建特权方法。第一种是在构造函数中实现,比如:

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

这个模式是把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访
问构造函数中定义的所有变量和函数的能力。

在上面的例子中,不能直接访问privateVariable 和privateFunction(),唯一的办法是使用publicMethod()。

如下面的例子所示,可以定义私有变量和特权方法,以隐藏不能被直接修改的数据:

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'

这段代码中的构造函数定义了两个特权方法:getName()和setName()。每个方法都可以构造函
数外部调用,并通过它们来读写私有的name 变量。在Person 构造函数外部,没有别的办法访问name。

因为两个方法都定义在构造函数内部,所以它们都是能够通过作用域链访问name 的闭包。

私有变量name 对每个Person 实例而言都是独一无二的,因为每次调用构造函数都会重新创建一套变量和方法。不过这样也有个问题:必须通过构造函数来实现这种隔离。正如第8 章所讨论的,构造函数模式的缺点是每个实例都会重新创建一遍新方法。使用静态私有变量实现特权方法可以避免这个问题。

10.16.1 静态私有变量

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

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

在这个模式中,匿名函数表达式创建了一个包含构造函数及其方法的私有作用域。首先定义的是私有变量和私有函数,然后又定义了构造函数和公有方法。公有方法定义在构造函数的原型上,与典型的原型模式一样。注意,这个模式定义的构造函数没有使用函数声明,使用的是函数表达式。

基于同样的原因(但操作相反),这里声明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()); // 'Nicholas'
person1.setName('Matt');
console.log(person1.getName()); // 'Matt'

let person2 = new Person('Michael');
console.log(person1.getName()); // 'Michael'
console.log(person2.getName()); // 'Michael'

这里的Person 构造函数可以访问私有变量name,跟getName()和setName()方法一样。使用这种模式,name 变成了静态变量,可供所有实例使用。这意味着在任何实例上调用setName()修改这个变量都会影响其他实例。

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


10.16.2 模块模式

在一个单例对象上实现了相同的隔离和封装。单例对象(singleton)就是只有一个实例的对象。按照惯例,JavaScript 是通过对象字面量来创建单例对象的,如下面的例子所示:

let singleton = {
	name: value,
	method() {
		// 方法的代码
	}
};

创建了一个单独的对象。

模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。模块模式的样板代码如下:

let singleton = function() {
	// 私有变量和私有函数
	let privateVariable = 10;
	function privateFunction() {
		return false;
	}
	
	// 特权/公有方法和属性
	return {
		publicProperty: true,
		publicMethod() {
			privateVariable++;
			return privateFunction();
		}
	};
}();

模块模式使用了匿名函数返回一个对象。在匿名函数内部,首先定义私有变量和私有函数。之后,创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。因为这个对象定义在匿名函数内部,所以它的所有公有方法都可以访问同一个作用域的私有变量和私有函数。

公有方法通过闭包可以访问私有变量和私有函数

在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有的数据,而这些数据又可以通过其暴露的公共方法来访问。以这种方式创建的每个单例对象都是Object 的实例,因为最终单例都由一个对象字面量来表示。

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

console.log(singleton.publicMethod()) // false

本质上还是通过闭包来访问私有变量,通过为返回对象添加内部函数,在内部函数中获取私有方法和变量。但无法直接获取私有方法和变量,实现了私有。

如上例在公共方法中调用了私有方法。



小结

函数是JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而让开发者可以更有效地使用函数。

  • 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数。
  • ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。
  • JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及ES6 新增的扩展操作符,
    可以实现函数定义和调用的完全动态化。
  • 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了什么参数等信息。
  • JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。
  • 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象。
  • 通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。
  • 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
  • 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
  • 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都 会被销毁。
  • 虽然JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域 中定义的变量。
  • 可以访问私有变量的公共方法叫作特权方法。
  • 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增 强模式在单例对象上实现。
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-25 12:06:48  更:2021-08-25 12:07:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:08:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码