递归
执行上下文和堆栈
- 有关正在运行的函数的执行过程的相关信息被存储在其 执行上下文 中
- 执行上下文是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,
this
的值(此处我们不使用它),以及其它的一些内部细节 - 一个函数调用仅具有一个与其相关联的执行上下文
嵌套调用
- 当一个函数进行嵌套调用时,将发生以下的事儿:
- 当前函数被暂停;
- 与它关联的执行上下文被一个叫做 执行上下文堆栈 的特殊数据结构保存;
- 执行嵌套调用;
- 嵌套调用结束后,从堆栈中恢复之前的执行上下文,并从停止的位置恢复外部函数
链表
- 如果我们确实需要快速插入/删除,则可以选择另一种叫做 链表的数据结构
- 链表元素 是一个使用以下元素通过递归定义的对象:
value
。next
属性引用下一个 链表元素 或者代表末尾的null
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let list = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null } } } }; |
1 2 3 4 5 |
let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; list.next.next.next.next = null; |
- 添加一个新值,我们需要更新链表的头:
1 2 3 4 5 6 7 |
let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; // 将新值添加到链表头部 list = { value: "new item", next: list }; |
- 要从中间删除一个值,可以修改前一个元素的
next
:
1 |
list.next = list.next.next; |
- 链表主要的缺点
- 无法很容易地通过元素的编号获取元素
Rest
参数与 Spread
语法
概述
- 在
JavaScript
中,很多内建函数都支持传入任意数量的参数Math.max(arg1, arg2, ..., argN)
—— 返回参数中的最大值。Object.assign(dest, src1, ..., srcN)
—— 依次将属性从src1..N
复制到dest
。
Rest
参数 ...
- 在
JavaScript
中,无论函数是如何定义的,你都可以在调用它时传入任意数量的参数- 例如
- 虽然这里这个函数不会因为传入过多的参数而报错。但是,当然,只有前两个参数被求和了
1 2 3 4 5 |
function sum(a, b) { return a + b; } alert( sum(1, 2, 3, 4, 5) ); |
- 可以在函数定义中声明一个数组来收集参数
- 语法是这样的:
...变量名
,这将会声明一个数组并指定其名称,其中存有剩余的参数 - 这三个点的语义就是“收集剩余的参数并存进指定数组中”
- 语法是这样的:
1 2 3 4 5 6 7 8 9 10 11 |
function sumAll(...args) { // 数组名为 args let sum = 0; for (let arg of args) sum += arg; return sum; } alert( sumAll(1) ); // 1 alert( sumAll(1, 2) ); // 3 alert( sumAll(1, 2, 3) ); // 6 |
- 也可以选择将第一个参数获取为变量,并将剩余的参数收集起来
1 2 3 4 5 6 7 8 9 10 11 |
function showName(firstName, lastName, ...titles) { alert( firstName + ' ' + lastName ); // Julius Caesar // 剩余的参数被放入 titles 数组中 // i.e. titles = ["Consul", "Imperator"] alert( titles[0] ); // Consul alert( titles[1] ); // Imperator alert( titles.length ); // 2 } showName("Julius", "Caesar", "Consul", "Imperator"); |
注意:Rest
参数
Rest
参数必须放到参数列表的末尾
“arguments
” 变量
- 有一个名为
arguments
的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function showName() { alert( arguments.length ); alert( arguments[0] ); alert( arguments[1] ); // 它是可遍历的 // for(let arg of arguments) alert(arg); } // 依次显示:2,Julius,Caesar showName("Julius", "Caesar"); // 依次显示:1,Ilya,undefined(没有第二个参数) showName("Ilya"); |
注意:箭头函数
- 在箭头函数中访问
arguments
,访问到的arguments
并不属于箭头函数,而是属于箭头函数外部的“普通”函数- 所以,箭头函数没有
"arguments"
- 好比,箭头函数没有自身的
this
- 所以,箭头函数没有
1 2 3 4 5 6 |
function f() { let showArg = () => alert(arguments[0]); showArg(); } f(1); // 1 |
Spread
语法
- 从数组中获取参数列表
- 例如,内建函数
Math.max
会返回参数中最大的值:- 如果我们有一个数组
[3, 5, 1]
,我们该如何用它调用Math.max
呢? - 直接“原样”传入这个数组是不会奏效的,因为
Math.max
期望的是列表形式的数值型参数,而不是一个数组:
- 如果我们有一个数组
1 |
alert( Math.max(3, 5, 1) ); // 5 |
1 2 3 |
let arr = [3, 5, 1]; alert( Math.max(arr) ); // NaN |
- 我们不能手动地去一一设置参数
Math.max(arg[0], arg[1], arg[2])
,因为我们不确定这儿有多少个- 在代码执行时,参数数组中可能有很多个元素,也可能一个都没有
Spread
语法 可以解决这个问题- 它看起来和
rest
参数很像,也使用...
,但是二者的用途完全相反 - 当在函数调用中使用
...arr
时,它会把可迭代对象arr
“展开”到参数列表中 - 以
Math.max
为例:
1 2 3 |
let arr = [3, 5, 1]; alert( Math.max(...arr) ); // 5(spread 语法把数组转换为参数列表) |
1 2 3 4 |
let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(...arr1, ...arr2) ); // 8 |
1 2 3 4 |
let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25 |
- 还可以使用
spread
语法来合并数组:
1 2 3 4 5 6 |
let arr = [3, 5, 1]; let arr2 = [8, 9, 15]; let merged = [0, ...arr, 2, ...arr2]; alert(merged); // 0,3,5,1,2,8,9,15(0,然后是 arr,然后是 2,然后是 arr2) |
- 上面的示例中,我们使用数组展示了
spread
语法,其实我们可以用spread
语法这样操作任何可迭代对象Spread
语法内部使用了迭代器来收集元素,与for..of
的方式相同- 对于这个特定任务,我们还可以使用
Array.from
来实现,因为该方法会将一个可迭代对象(如字符串)转换为数组:
1 2 3 |
let str = "Hello"; alert( [...str] ); // H,e,l,l,o |
1 2 3 4 |
let str = "Hello"; // Array.from 将可迭代对象转换为数组 alert( Array.from(str) ); // H,e,l,l,o |
- 不过
Array.from(obj)
和[...obj]
存在一个细微的差别:Array.from
适用于类数组对象也适用于可迭代对象Spread
语法只适用于可迭代对象
复制 array/object
- 使用
spread
语法也可以做同样的事情(译注:也就是进行浅拷贝)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let arr = [1, 2, 3]; let arrCopy = [...arr]; // 将数组 spread 到参数列表中 // 然后将结果放到一个新数组 // 两个数组中的内容相同吗? alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true // 两个数组相等吗? alert(arr === arrCopy); // false(它们的引用是不同的) // 修改我们初始的数组不会修改副本: arr.push(4); alert(arr); // 1, 2, 3, 4 alert(arrCopy); // 1, 2, 3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let obj = { a: 1, b: 2, c: 3 }; let objCopy = { ...obj }; // 将对象 spread 到参数列表中 // 然后将结果返回到一个新对象 // 两个对象中的内容相同吗? alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true // 两个对象相等吗? alert(obj === objCopy); // false (not same reference) // 修改我们初始的对象不会修改副本: obj.d = 4; alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4} alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3} |
变量作用域,闭包
概述
JavaScript
是一种非常面向函数的语言。它给了我们很大的自由度- 在
JavaScript
中,我们可以随时创建函数,可以将函数作为参数传递给另一个函数,并在完全不同的代码位置进行调用
- 在
let/const
- 在
JavaScript
中,有三种声明变量的方式:let
,const
(现代方式),var
(过去留下来的方式)- 用
const
声明的变量的行为也相同(译注:与let
在作用域等特性上是相同的) - 旧的
var
与上面两个有着明显的区别
- 用
代码块
- 如果在代码块
{...}
内声明了一个变量,那么这个变量只在该代码块内可见
1 2 3 4 5 6 7 8 9 |
{ // 使用在代码块外不可见的局部变量做一些工作 let message = "Hello"; // 只在此代码块内可见 alert(message); // Hello } alert(message); // Error: message is not defined |
1 2 3 4 5 6 7 8 9 10 11 |
{ // 显示 message let message = "Hello"; alert(message); } { // 显示另一个 message let message = "Goodbye"; alert(message); } |
- 这里如果没有代码块则会报错
1 2 3 4 5 6 7 |
// 显示 message let message = "Hello"; alert(message); // 显示另一个 message let message = "Goodbye"; // Error: variable already declared alert(message); |
嵌套函数
- 如果一个函数是在另一个函数中创建的,该函数就被称为“嵌套”函数
- 这里创建的 嵌套 函数
getFullName()
是为了更加方便 - 它可以访问外部变量,因此可以返回全名。嵌套函数在
JavaScript
中很常见
- 这里创建的 嵌套 函数
1 2 3 4 5 6 7 8 9 10 11 |
function sayHiBye(firstName, lastName) { // 辅助嵌套函数使用如下 function getFullName() { return firstName + " " + lastName; } alert( "Hello, " + getFullName() ); alert( "Bye, " + getFullName() ); } |
- 更有意思的是,可以返回一个嵌套函数:作为一个新对象的属性或作为结果返回
- 之后可以在其他地方使用。不论在哪里调用,它仍然可以访问相同的外部变量
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2 |
词法环境
- 在
JavaScript
中,每个运行的函数,代码块{...}
以及整个脚本,都有一个被称为 词法环境(Lexical Environment
) 的内部(隐藏)的关联对象 - 词法环境对象由两部分组成:
- 环境记录(
Environment Record
) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如this
的值)的对象 - 对 外部词法环境 的引用,与外部代码相关联
- 环境记录(
词法环境:变量
- 一个“变量”只是 环境记录 这个特殊的内部对象的一个属性
- “获取或修改变量”意味着“获取或修改词法环境的一个属性”
- 举个例子,这段没有函数的简单的代码中只有一个词法环境:
- 这就是所谓的与整个脚本相关联的 全局 词法环境
1 2 |
let phr = "hello" alert(phr) |
注意:词法环境
- “词法环境”是一个规范对象(
specification object
):它只存在于 语言规范的“理论”层面,用于描述事物是如何工作的 - 我们无法在代码中获取该对象并直接对其进行操作
词法环境:函数声明
- 一个函数其实也是一个值,就像变量一样
- 不同之处在于函数声明的初始化会被立即完成
- 当创建了一个词法环境(
Lexical Environment
)时,函数声明会立即变为即用型函数(不像let
那样直到声明处才可用)- 这就是为什么我们甚至可以在声明自身之前调用一个以函数声明(
Function Declaration
)的方式声明的函数
- 这就是为什么我们甚至可以在声明自身之前调用一个以函数声明(
内部和外部的词法环境
- 在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数
- 在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):
- 内部词法环境与
say
的当前执行相对应。它具有一个单独的属性:name
,函数的参数。我们调用的是say("John")
,所以name
的值为"John"
- 外部词法环境是全局词法环境。它具有
phr
变量和函数本身
1 2 3 4 5 6 7 |
let phr = "hello" function say(name) { alert('${phr}, ${name}') } say("john") |
- 当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境
词法环境:返回函数
- 回到
makeCounter
这个例子- 在每次
makeCounter()
调用的开始,都会创建一个新的词法环境对象,以存储该makeCounter
运行时的变量 - 因此,我们有两层嵌套的词法环境
- 不同的是,在执行
makeCounter()
的过程中创建了一个仅占一行的嵌套函数:return count++
。我们尚未运行它,仅创建了它 - 所有的函数在“诞生”时都会记住创建它们的词法环境
从技术上讲,这里没有什么魔法:所有函数都有名为[[Environment]]
的隐藏属性,该属性保存了对创建该函数的词法环境的引用 - 因此,
counter.[[Environment]]
有对{count: 0}
词法环境的引用
这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关
[[Environment]]
引用在函数创建时被设置并永久保存 - 稍后,当调用
counter()
时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于counter.[[Environment]]
: - 当
counter()
中的代码查找count
变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部makeCounter()
的词法环境,并且在哪里找到就在哪里修改
- 在每次
1 2 3 4 5 6 7 8 9 10 11 |
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); counter() |
闭包
- 指一个函数可以记住其外部变量并可以访问这些变量
- 在某些编程语言中,这是不可能的,或者应该以一种特殊的方式编写函数来实现
- 但如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,"
new Function
" )
垃圾回收
- 通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除
- 因为现在没有任何对它们的引用了
- 与
JavaScript
中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中
- 但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的
[[Environment]]
属性- 下面这个例子中,即使在(外部)函数执行完成后,它的词法环境仍然可达
- 因此,此词法环境仍然有效
1 2 3 4 5 6 7 8 9 |
function f() { let value = 123; return function() { alert(value); } } let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用 |
变量
var
声明与let
相似。大部分情况下,我们可以用let
代替var
或者var
代替let
,都能达到预期的效果:
1 2 |
var message = "Hi"; alert(message); // Hi |
var
- 用
var
声明的变量,不是函数作用域就是全局作用域- 它们在代码块外也是可见的(译注:也就是说,
var
声明的变量只有函数作用域和全局作用域,没有块级作用域) - 由于
var
会忽略代码块,因此我们有了一个全局变量test
- 它们在代码块外也是可见的(译注:也就是说,
1 2 3 4 5 |
if (true) { var test = true; // 使用 "var" 而不是 "let" } alert(test); // true,变量在 if 结束后仍存在 |
1 2 3 4 5 6 7 |
for (var i = 0; i < 10; i++) { var one = 1; // ... } alert(i); // 10,"i" 在循环结束后仍可见,它是一个全局变量 alert(one); // 1,"one" 在循环结束后仍可见,它是一个全局变量 |
1 2 3 4 5 6 7 8 9 10 |
function sayHi() { if (true) { var phrase = "Hello"; } alert(phrase); // 能正常工作 } sayHi(); alert(phrase); // ReferenceError: phrase is not defined |
var
允许重新声明
1 2 |
let user; let user; // SyntaxError: 'user' has already been declared |
1 2 3 4 5 6 |
var user = "Pete"; var user = "John"; // 这个 "var" 无效(因为变量已经声明过了) // ……不会触发错误 alert(user); // John |
- “
var
” 声明的变量,可以在其声明语句前被使用- 这种行为称为“提升”
- 因为所有的
var
都被“提升”到了函数的顶部
1 2 3 4 5 6 7 8 |
function sayHi() { phrase = "Hello"; alert(phrase); var phrase; } sayHi(); |
IIEF
- 在之前,
JavaScript
中只有var
这一种声明变量的方式,并且这种方式声明的变量没有块级作用域 - 程序员们就发明了一种模仿块级作用域的方法。这种方法被称为“立即调用函数表达式”(
immediately-invoked function expressions
,IIFE
)- 如今,我们不应该再使用
IIFE
了,但是你可以在旧脚本中找到它们
- 如今,我们不应该再使用
1 2 3 4 5 6 7 |
(function() { var message = "Hello"; alert(message); // Hello })(); |
- 创建方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 创建 IIFE 的方法 (function() { alert("Parentheses around the function"); })(); (function() { alert("Parentheses around the whole thing"); }()); !function() { alert("Bitwise NOT operator starts the expression"); }(); +function() { alert("Unary plus starts the expression"); }(); |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!