Skip to content

递归和堆栈

  1. 递归
    1. 函数会调用 自身。这就是所谓的 递归
    2. 两种思考方式
      1. 迭代思路:使用 for 循环
        1. 递归思路:简化任务,调用自身
    JavaScript
    	function pow(x, n) {
    	  return (n == 1) ? x : (x * pow(x, n - 1));
    	} 
    	//最大的嵌套调用次数(包括首次)被称为 递归深度
    1. 执行上下文和堆栈
      1. 有关正在运行的函数的执行过程的相关信息被存储在其 执行上下文 中。
      2. 执行上下文 是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,this 的值(此处我们不使用它),以及其它的一些内部细节。
  2. 出口
    1. 任何递归都可以用循环来重写。通常循环变体更有效。
  3. 递归遍历
  4. 递归结构
  5. 链表
    1. value
    2. next 属性引用下一个 链表元素 或者代表末尾的 null

Rest 参数与 Spread 语法

  • Math.max(arg1, arg2, ..., argN) —— 返回参数中的最大值。
  • Object.assign(dest, src1, ..., srcN) —— 依次将属性从 src1..N 复制到 dest
  1. Rest 参数 ...
    1. Rest 参数必须放到参数列表的末尾
  2. “arguments” 变量
    1. 有一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。
    2. 箭头函数没有 "arguments"
  3. Spread 语法
  4. 复制 array/object

变量作用域,闭包

JavaScript 是一种非常面向函数的语言。 在 JavaScript 中,有三种声明变量的方式:letconst(现代方式),var(过去留下来的方式)

  1. 代码块
    1. 如果在代码块 {...} 内声明了一个变量,那么这个变量只在该代码块内可见
  2. 嵌套函数
    1. 如果一个函数是在另一个函数中创建的,该函数就被称为“嵌套”函数。
  3. Step 1. 变量
    1. 在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
    2. 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
    3. 对 外部词法环境 的引用,与外部代码相关联。
    4. 词法环境是一个规范对象
      1. “词法环境”是一个规范对象(specification object):它只存在于 语言规范 的“理论”层面,用于描述事物是如何工作的。我们无法在代码中获取该对象并直接对其进行操作。
  4. Step 2. 函数声明
    1. 不同之处在于函数声明的初始化会被立即完成。
  5. Step 3. 内部和外部的词法环境
    1. 当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
  6. Step 4. 返回函数
    1. 在变量所在的词法环境中更新变量。
  7. 闭包
    1. 闭包 是指一个函数可以记住其外部变量并可以访问这些变量。在某些编程语言中,这是不可能的,或者应该以一种特殊的方式编写函数来实现。但如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。
  8. 垃圾收集
    1. 函数调用完成后,会将词法环境和其中的所有变量从内存中删除
    2. 但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]] 属性。
    3. 当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。
  9. 实际开发中的优化
    1. 如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
    2. 在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
JavaScript

	let name = "John";
	function sayHi() {
	  alert("Hi, " + name);
	}
	name = "Pete";
	sayHi(); // 会显示 "Pete"?

老旧的 “var”

  1. “var” 没有块级作用域
    1. 用 var 声明的变量,不是函数作用域就是全局作用域
  2. “var” 允许重新声明
JavaScript
	var user = "Pete";
	var user = "John"; // 这个 "var" 无效(因为变量已经声明过了)
	// ……不会触发错误
	alert(user); // John
  1. “var” 声明的变量,可以在其声明语句前被使用
    1. 当函数开始的时候,就会处理 var 声明(脚本启动对应全局变量)
    2. 与它在代码中定义的位置无关(这里不考虑定义在嵌套函数中的情况)。
  2. 声明会被提升,但是赋值不会。
JavaScript
	function sayHi() {
	  alert(phrase);  //undefined
	
	  var phrase = "Hello";
	}
	
	sayHi();
赋值操作始终是在它出现的地方才起作用
  1. IIFE
    1. JavaScript 中只有 var 这一种声明变量的方式,并且这种方式声明的变量没有块级作用域,程序员们就发明了一种模仿块级作用域的方法。这种方法被称为“立即调用函数表达式”(immediately-invoked function expressions,IIFE)。
    2. 如今不再使用

全局对象

在浏览器中,它的名字是 “window”,对 Node.js 而言,它的名字是 “global”,

  1. 全局对象
    1. 全局对象的所有属性都可以被直接访问
    2. 使用 var(而不是 let/const!)声明的全局函数和变量会成为全局对象的属性
  2. 使用 polyfills

函数对象,NFE

  1. 属性 “name”
    1. 函数名.name ,返回函数名字
  2. 属性 “length”
    1. 返回函数的参数个数
    2. rest 参数不参与计数。
  3. 自定义属性
    1. 属性不是变量
JavaScript
	function sayHi() {
	  alert("Hi");
	
	  // 计算调用次数
	  sayHi.counter++;
	}
	sayHi.counter = 0; // 初始值
	
	sayHi(); // Hi
	sayHi(); // Hi
	
	alert( `Called ${sayHi.counter} times` ); // Called 2 times
  1. 命名函数表达式
    1. 取一个函数自己的别名,在函数内部调用
  2. 没有函数声明这样的说法

“new Function”语法

  1. 语法
    1. let func = new Function ([arg1, arg2, ...argN], functionBody);
    2. 使用 new Function 创建函数的应用场景非常特殊,比如在复杂的 Web 应用程序中,我们需要从服务器获取代码或者动态地从模板编译函数时才会使用。
    3. 使用 new Function 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境。
  2. 闭包
    1. 闭包是指使用一个特殊的属性 [[Environment]] 来记录函数自身的创建时的环境的函数。
  3. 即使我们可以在 new Function 中访问外部词法环境,我们也会受挫于压缩程序。
  4. 在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(minifier) 对其进行压缩 —— 一个特殊的程序,通过删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量。

调度:setTimeout 和 setInterval

有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行。这就是所谓的“计划调用(scheduling a call)”。

  • setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。
  • setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。
  • 参数都是毫秒单位
  1. setTimeout
    1. let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
      1. func|code:想要执行的函数或代码字符串。 一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做。使用函数名,而不是函数名( ),因为这里是引用函数,不是执行函数
      2. delay:执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0;
      3. arg1arg2:要传入被执行函数(或代码字符串)的参数列表(IE9 以下不支持)
  2. 用 clearTimeout 来取消调度 1. setTimeout 在调用时会返回一个“定时器标识符(timer identifier)”,在我们的例子中是 timerId,我们可以使用 clearTimeout 来取消执行。
JavaScript
		let timerId = setTimeout(() => alert("never happens"), 1000);
		alert(timerId); // 定时器标识符
		clearTimeout(timerId);
		alert(timerId); // 还是这个标识符(并没有因为调度被取消了而变成 null)
  1. setInterval
    1. setInterval 方法和 setTimeout 的语法相同:
    2. let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
    3. 与 setTimeout 只执行一次不同,setInterval 是每间隔给定的时间周期性执行。
  2. 嵌套的 setTimeout
    1. 周期性调度有两种方式:一种是使用 setInterval,另外一种就是嵌套的 setTimeout
    2. 嵌套的 setTimeout 相较于 setInterval 能够更精确地设置两次执行之间的延时。
    3. 使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短!
    4. 嵌套的 setTimeout 就能确保延时的固定
    5. 垃圾回收和 setInterval/setTimeout 回调(callback):当一个函数传入 setInterval/setTimeout 时,将为其创建一个内部引用,并保存在调度程序中。这样,即使这个函数没有其他引用,也能防止垃圾回收器(GC)将其回收。
  3. 零延时的 setTimeout
    1. 特殊的用法:setTimeout(func, 0),或者仅仅是 setTimeout(func)
    2. 这样调度可以让 func 尽快执行。但是只有在当前正在执行的脚本执行完成后,调度程序才会调用它。也就是说,该函数被调度在当前脚本执行完成“之后”立即执行。
    3. 零延时实际上不为零(在浏览器中)
      1. 根据 HTML5 标准 所讲:“经过 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒”。
JavaScript
	setTimeout(() => alert("World"));
	alert("Hello");
	//会先输出 “Hello”,然后立即输出 “World”

装饰器模式和转发 call,apply

JavaScript 在处理函数时提供了非凡的灵活性。它们可以被传递,用作对象,现在我们将看到如何在它们之间 转发(forward) 调用并 装饰(decorate) 它们。

  1. 透明缓存
    1. 函数的结果是稳定的,创建一个包装器(wrapper)函数,该函数增加了缓存功能
    2. cachingDecorator 是一个 装饰器(decorator):一个特殊的函数,它接受另一个函数并改变它的行为。
JavaScript
	function slow(x) {
  // 这里可能会有重负载的 CPU 密集型工作
  alert(`Called with ${x}`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // 如果缓存中有对应的结果
      return cache.get(x); // 从缓存中读取结果
    }

    let result = func(x);  // 否则就调用 func

    cache.set(x, result);  // 然后将结果缓存(记住)下来
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) 被缓存下来了,并返回结果
alert( "Again: " + slow(1) ); // 返回缓存中的 slow(1) 的结果

alert( slow(2) ); // slow(2) 被缓存下来了,并返回结果
alert( "Again: " + slow(2) ); // 返回缓存中的 slow(2) 的结果
  1. 使用 “func.call” 设定上下文
JavaScript
	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.call(this, x); // 现在 "this" 被正确地传递了
	    cache.set(x, result);
	    return result;
	  };
	}
	
	worker.slow = cachingDecorator(worker.slow); // 现在对其进行缓存
	
	alert( worker.slow(2) ); // 工作正常
	alert( worker.slow(2) ); // 工作正常,没有调用原始函数(使用的缓存)
  1. 传递多个参数
JavaScript
	let worker = {
  slow(min, max) {
    alert(`Called with ${min},${max}`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
  1. func.apply
    1. 我们可以使用 func.apply(this, arguments) 代替 func.call(this, ...arguments)
    2. func.apply(context, args)
    3. call 和 apply 之间唯一的语法区别是,call 期望一个参数列表,而 apply 期望一个包含这些参数的类数组对象。
      • Spread 语法 ... 允许将 可迭代对象 args 作为列表传递给 call
    4. apply 只接受 类数组 args。
  2. 借用一种方法
JavaScript
	function hash(args) {
	  return args[0] + ',' + args[1];
	}
	它仅适用于两个参数。如果它可以适用于任何数量的 `args` 就更好了
	自然的解决方案是使用 arr.join 方法:
	function hash(args) {
	  return args.join();
	}
	//另外一种方法
	function hash() {
	  alert( [].join.call(arguments) ); // 1,2
	}
	
	hash(1, 2);
  1. 装饰器和函数属性
    1. 通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西。如果原始函数有属性,例如 func.calledCount 或其他,则装饰后的函数将不再提供这些属性。因为这是装饰器。因此,如果有人使用它们,那么就需要小心。
    2. 例如,在上面的示例中,如果 slow 函数具有任何属性,而 cachingDecorator(slow) 则是一个没有这些属性的包装器。
    3. 一些包装器可能会提供自己的属性。例如,装饰器会计算一个函数被调用了多少次以及花费了多少时间,并通过包装器属性公开(expose)这些信息。
    4. 存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的 Proxy 对象来包装函数。我们将在后面的 Proxy 和 Reflect 中学习它。

函数绑定

  1. 丢失 “this”
    1. 一旦方法被传递到与对象分开的某个地方 —— this 就丢失。
JavaScript
	let user = {
	  firstName: "John",
	  sayHi() {
	    alert(`Hello, ${this.firstName}!`);
	  }
	};
	
	setTimeout(user.sayHi, 1000); // Hello, undefined!
  1. 解决方案 1:包装器
JavaScript
	let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000);
  1. 解决方案 2:bind
JavaScript
let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user); // (*)

// 可以在没有对象(译注:与对象分离)的情况下运行它
sayHi(); // Hello, John!

setTimeout(sayHi, 1000); // Hello, John!

// 即使 user 的值在不到 1 秒内发生了改变
// sayHi 还是会使用预先绑定(pre-bound)的值,该值是对旧的 user 对象的引用
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};
  1. 部分(应用)函数(Partial functions)
    1. bind: let bound = func.bind(context, [arg1], [arg2], ...);
  2. 在没有上下文情况下的 partial
JavaScript
	function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

// 用法:
let user = {
  firstName: "John",
  say(time, phrase) {
    alert(`[${time}] ${this.firstName}: ${phrase}!`);
  }
};

// 添加一个带有绑定时间的 partial 方法
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello");
// 类似于这样的一些内容:
// [10:00] John: Hello!

深入理解箭头函数

  • arr.forEach(func) —— forEach 对每个数组元素都执行 func
    • setTimeout(func) —— func 由内建调度器执行。
  1. 箭头函数没有 “this”
    1. 不能对箭头函数进行 new 操作
    2. :箭头函数不能用作构造器(constructor)。不能用 new 调用它们。
  2. 箭头函数没有 “arguments”