递归
递归是一种编程模式,用于一个任务可以被分割为多个相似的更简单的任务的场景。或者用于一个任务可以被简化为一个容易的行为加上更简单的任务变体当一个函数解决一个任务时,在该过程中它可以调用很多其它函数。那么当一个函数调用自身时,就称其为递归
递归深度–最大的嵌套调用次数(包括首次)
最大递归深度受限于 JavaScript 引擎。我们可以确信基本是 10000,有些引擎可能允许更大,但是 100000 很可能就超过了限制。有一些自动优化能够缓解这个(「尾部调用优化」),但是它们还没有被完全支持,只能用于简单场景
函数调用的工作原理
执行上下文:是一个内部数据结构,包含一个函数执行时的细节
函数发生嵌套调用时
- 当前函数被暂停
- 与它关联的执行上下文被一个叫做执行上下文堆栈的特殊数据结构保存
- 执行嵌套调用
- 调用结束后,执行上下文从堆栈中恢复,外部函数从停止的地方继续执行
示例
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
- 调用 pow(2,3),执行上下文存储变量:x=2,n=3.我们将执行上下文描述为如下形式(实际上这个更复杂)
Context: {x:2,n:3,at line 1} call:pow(2,3)
- 条件 n== 1 结果为false,进入else分支,执行上下文变为
Context:{x:2,n:3,at line 5} call:pow(2,3)
- 要执行 pow(2,2)了,JavaScript会记住执行上下文堆栈中的当前执行上下文,处理如下{
(1). 当前上下文被记录在堆栈顶部
(2). 为子调用创建新上下文
(3). 当子调用结束后,前一上下从堆栈弹出,继续执行
} - 进入子调用的上下文堆栈
Context:{x:2,n:2,at line 1} call:pow(2,2)
- 重复该过程(新的执行上下文被创建,前一个被压入堆栈顶部)
- 当 n== 1时,此时不再有嵌套调用,所以函数结束,返回1,它的执行上下文不再有用,会从内存中移除,前一上下文从栈顶被恢复
递归遍历
<script>
/* 递归遍历 */
let company= {
sales:[
{name:'叶叶',salary:1000},
{name:'Alice',salary:600}
],
development:{
sites:[
{name:'Peter',salary:2000},
{name:'Alex',salary:1800}
],
internals: [
{name: 'Jack',salary: 1300}
]
}
};
function sumSalaries(department){
if(Array.isArray(department)){
return department.reduce((prev,current)=> prev +current.salary,0);
}else{
let sum = 0;
for(let subdep of Object.values(department)){
sum += sumSalaries(subdep);
}
return sum;
}
}
alert(sumSalaries(company));
</script>
Rest参数(剩余参数…)
Rest 参数的操作符表示为3个点 … 直白地讲,它的意思就是“把剩余的参数都放到一个数组中”。
function sumAll(...args){
let sum = 0;
for(let arg of args){
sum += arg;
}
return sum;
}
alert(sumAll(1));
alert(sumAll(1,2));
alert(sumAll(1,2,3));
arguments 变量
函数的上下文会提供一个非常特殊的类数组对象 arguments,所有的参数被按序放置
function showName(){
alert(arguments.length);
alert(arguments[0]);
alert(arguments[1]);
}
showName('叶叶','花花');
showName('kuilei');
Spread操作符(展开操作符)
把可迭代对象展开为参数列表
let arr = [3,5,1];
alert(Math.max(...arr)); //把数组转化为参数列表
let arr2 = [8, 3, -8, 1];
alert(Math.max(...arr,...arr2));
示例
//合并数组
let merged=[0,...arr,2,...arr2];
console.log(merged);
// 展开字符数组
let str ="Hello";
console.log([...str]);
// 也可以使用Array.from,该操作符会将可遍历对象(如字符串)转换为数组
console.log(Array.from(str));
// 注: Array.form同时适用于类数组对象和可遍历对象
// Spread(展开操作符)只能操作可遍历对象
闭包
词法环境
在JavaScript中,每个运行的函数,代码块或者整个程序,都有一个称为词法环境的关联对象
词法环境=环境记录+外部词法环境的引用
环境记录:把所有局部变量作为其属性(包括一些额外信息,比如this)的对象
外部词法环境的引用:嵌套当前代码(当前花括号之外)之外的词法环境
因此:变量 是环境记录这个特殊内部对象的属性,访问或修改变量 就是在访问或修改词法环境的一个属性
函数声明
原则:当代码执行到它们时,它们并不会立即执行,而是在词法环境创建完成后才会执行(对于全局词法环境,它意味着脚本启动的那一刻)
函数执行细节
<script>
let phrase="Hello";
function say(name){
alert(`${phrase},${name}`);
}
say();
</script>
- 当脚本加载完成时,它会创建一个全局词法环境
- 所有函数在创建时都会根据创建它的词法环境获得隐藏的[[Environment]]属性.say()创建于全局词法环境,那么[[Environment]]中保留它的一个引用
- 当执行到函数 say(),会自动创建一个新的函数词法环境,这个词法环境存储调用的局部变量和参数,并且它的外部词法环境的引用[[Environment]]指向全局词法环境
- 当代码试图访问一个变量时,它首先会在内部词法环境中进行搜索,然后在它外部词法环境引用指向的词法环境中搜索,然后是更外部的环境,直到词法环境链的末端.
- 当在词法环境中找到一个变量时,就地修改此变量的值(如果查看该变量的值,使用的是变量的最新值)
注:
(1).每次函数运行会都会创建一个新的函数词法环境。
(2).一个函数被调用多次,那么每次调用也都会此创建一个拥有指定局部变量和参数的词法环境。
闭包
函数保存其外部的变量并且能够访问它们称之为闭包。他们会通过隐藏的 [[Environment]] 属性记住创建它们的位置,所以它们都可以访问外部变量。
对于{…}代码块,if,for,while,语法环境也是存在的
IIFE(立即调用函数表达式)
(function() {
let message = "Hello";
alert(message); // Hello
})();
注:函数表达式被括号 (function {…}) 包裹起来,因为在 JavaScript 中,当代码流碰到 “function” 时,它会把它当成一个函数声明的开始。但函数声明必须有一个函数名,所以会导致错误.需要使用圆括号告诉给 JavaScript,这个函数是在另一个表达式的上下文中创建的,因此它是一个表达式。它不需要函数名也可以立即调用。
函数对象
属性:
- name:函数名
- length:函数定义时传入参数的个数,余参不参与计数。
命名函数表达式(NFE)
带有名字的函数表达式的术语
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
名字 func,有两个特殊的地方:
- 它允许函数在内部引用自己。
- 它在函数外是不可见的。
通过字符串创建函数
let func = new Function ([arg1[, arg2[, ...argN]],] functionBody);
arg1,arg2...表示函数的参数(准确的说是形参名)
functionBody表示函数体
所有参数都是字符串
使用 new Function 创建函数,函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境。因此,我们不能在新函数中直接使用外部变量。当要从服务器获取代码或者动态地按模板编译函数时才会使用,在一般的程序开发中很少使用。
var
- var 没有块作用域(var声明的变量,不是函数范围就是全局的)
- var 声明在函数开始时处理(全局声明在脚本开始时处理)
- 顶级 var 变量和函数声明后会自动成为window的属性
全局对象
全局对象提供可在任何地方使用的变量和函数。大多数情况下,这些全局变量内置于语言或主机环境中。浏览器中它被命名为 “window”,对 Node.JS 而言是 “global”,其它环境可能用的别的名字。
<script>
/* 1. 缓存装饰器(不适用于对象方法) */
/* function slow(x) {
// 这里可能会有重负载的CPU密集型工作
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function (x) {
if (cache.has(x)) { // 如果结果在 map 里
return cache.get(x); // 返回它
}
let result = func(x); // 否则就调用函数
cache.set(x, result); // 然后把结果缓存起来
return result;
};
}
slow = cachingDecorator(slow);
alert(slow(1)); // slow(1) 被缓存起来了
alert("Again: " + slow(1)); // 一样的
alert(slow(2)); // slow(2) 被缓存起来了
alert("Again: " + slow(2)); // 也是一样 */
/* 2. 使用 “func.call” 作为上下文
func.call(context, arg1, arg2, ...)
它运行 func,提供的第一个参数作为 this,后面的作为参数。
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert('Called with ' + x);
return x * this.someMethod();
}
};
function cachingDecorator(func) {
let cache = new Map();
return function (x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x); // (**)
cache.set(x, result);
return result;
};
}
alert(worker.slow(1)); // Called with 1;
worker.slow=cachingDecorator(worker.slow); //让它缓存起来
// alert(worker.slow(2)); //函数深入.html:282 Uncaught TypeError: this.someMethod is not a function
// 包装器将调用传递给原始方法,但没有上下文 this。因此错误。
// 解决办法:有一个特殊的内置函数方法 func.call(context, …args),允许调用一个显式设置 this 的函数。它运行 func,提供的第一个参数作为 this,后面的作为参数。
function sayHi(){
alert(this.name);
}
let user ={name:'叶叶'};
let admin = {name:'花花'};
// 使用call将不同的对象传递为this
sayHi.call(user);
sayHi.call(admin);
// 改进后的
function cachingDecorator(func) {
let cache = new Map();
return function (x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this,x);
cache.set(x, result);
return result;
};
}
*/
/* 3. 使用 “func.apply” 来传递多参数
func.apply(context, args)
它运行 func 设置 this=context 并使用类似数组的对象 args 作为参数列表。
apply 只接受 类似数组一样的 参数列表。
*/
function say(time,phrase){
alert(`[${time}] ${this.name}:${phrase}`);
}
let user = {name: '叶叶'};
let messageData = ['10:00','Hello'];
// user成为this,messageData作为参数列表传递
say(user,messageData);
/* 4. func.apply和func.call的区别
1.call允许将可迭代的参数列表传递给call
2.apply只接受类型数组一样的参数列表
如果 参数列表 既可迭代又像数组一样,就像真正的数组一样,那么我们在技术上可以使用它们中的任何一个,但是 apply 可能会更快,因为它只是一个操作。大多数 JavaScript 引擎内部优化比一对 call + spread 更好。
*/
/* 5. func.apply的应用,呼叫转移 */
let wrapper = function(){
return anotherFunction.apply(this,arguments);
}
/* 6. 借用一种方法 */
/* function hash(){
alert(arguments.join()); //报错,因为arguments不是数组对象,不能调用数组方法(虽然它即可迭代又像数组一样)
// Uncaught TypeError: arguments.join is not a function
}
hash(1,2); */
// 方法借用
function hash(){
alert([].join.call(arguments));
}
hash(1,2);
</script>
函数绑定
方法 func.bind(context, …args) 返回了一个函数 func 的“边界变量”,它固定了上下文 this 和参数(如果给定了)。
通常我们应用 bind 来固定对象方法的 this,这样我们就可以把它们传递到其他地方使用。例如,传递给 setTimeout。
<script>
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...在一秒之内
user = {
sayHi() {
alert("Another user in setTimeout!");
}
};
// 在 setTimeout 中是另外一个 user 了?!? */
// 函数绑定
/* let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John */
// 修正第一个问题
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user);
sayHi();
setTimeout(sayHi,1000);
// ...在一秒之内
user = {
sayHi() {
alert("Another user in setTimeout!");
}
};
user.sayHi();
// 如果有一个对象有很多方法,并且我们都打算将它们传递出去使用,那么我们可以在一个循环中完成绑定
for(let key in user){
if(typeof user[key] == 'function'){
user[key] = user[key].bind(user);
}
}
</script>