笔记参考javascript.info中文站
变量作用域,闭包
Javascript 面向函数的特点非常明显,大量的函数会让我们产生一些问题:外部函数会怎样?内部参数调用呢?返回值会不会影响旧值呢?
这些问题本质上,其实都是作用域和闭包的问题
1. 代码块
使用 {}
来声明一个代码块,代码块内的变量只能在代码块中使用,起到了一个隔离的作用
举个例子:
{
// 使用在代码块外不可见的局部变量做一些工作
let message = "Hello"; // 只在此代码块内可见
alert(message); // Hello
}
alert(message); // Error: message is not defined
对于 if
、for
、while
循环而言也是一样
2. 嵌套函数
函数中创建的函数被称为嵌套函数:
function sayHiBye(firstName, lastName) {
// 辅助嵌套函数使用如下
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
嵌套函数可以调用外部变量 firstName, lastName
我们也可以把函数作为返回结果直接返回,这样的函数也被称作嵌套函数,而且这样调用函数的时候可以只使用外部函数的变量而不运行外部函数:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
非常实用,像这个例子本身就是一个可读性很高的计数器
3. 词法环境
3.1 变量
每个代码块都有一个 “词法环境”,词法环境是一个对象,用来存储该代码块的所有局部变量和一些其他信息(如 this 的值),以及外部词法环境
其键值对的内容会随着变量内容的转变而转变,在程序运行之初就会给在对象中给每个变量分配一个键,值待定:
3.2 函数声明
函数声明的初始化会被立即完成,当 “词法环境” 创建时,函数立刻可以被使用,这就是为什么函数可以在定义前就可以被调用
但只有函数声明时才使用,给变量赋值函数如 let say = function(name)...
并不适用
3.3 内部和外部的词法环境
词法环境分内外,有可能会嵌套很多层,当代码像访问一个变量时,首先会从当前层开始搜索,无果后使用 outer
再搜索外面一层,依次向外搜索直到找到变量
如果到全局变量一层仍未找到,则报错(严格模式)或重新创建一个新变量(非严格)
举个例子:
函数被调用时会执行 alert()
语句,在调用 phrase
时在内部找不到,需要到外部环境去找;调用 name
时则直接在内部找到了变量
3.4 返回函数
在上面 makeCounter()
的例子中,我们注意到,即使调用了函数,count
变量也不会被重定义成 0
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
这是因为在调用发挥函数时,需要查找 count
变量,内部找不到之后去外部 makeCounter()
函数中寻找,又因为 makeCounter()
函数没有被重复调用,所以词法环境一直没变,所以寻找到的 count
变量就是以前定义过的 count
变量
返回函数的词法环境很特殊,在外层函数被调用时,前置语句运行完后,在运行返回函数前会创建返回函数,函数创建之初就会记录下创建他们的词法环境,并将其记录在隐藏属性 [[Environment]]
中
返回函数的 [[Environment]]
不断更新,而外部函数的 [[Environment]]
一直不变,因此 count
变量不会刷新
这还涉及到了 Javascript 函数的一个特点:天生闭包
闭包指的是函数可以记住其外部变量并可以访问这些变量,因为 [[Environment]]
属性会帮助函数记录他们自己的位置和其他函数的位置,搞清内外部的关系,从而可以访问自身所在一层以及外层的变量
4. 垃圾收集
函数调用完成后,会将词法环境和其中的所有变量从内存中删除,因为调用完成后对它们的引用已经不存在,但有时,例如上面的 count 例子,返回函数在外部函数调用完成后依然可以调用,因为它被保存在了一个变量中,引用依然存在,那么此时的词法环境和 [[Environment]]
属性就不会删除
但是注意,如果多次调用这些返回函数,返回的结果会分别保存在内存中,而不会被替代成一个
在实际开发中,V8 引擎考虑到内存有限,为了优化运行过程,引擎会自动将很明显就不会被使用的外部变量删除,这就会导致有些情况下,当我们使用浏览器调试时,从内部词法环境的角度无法监视明明存在的外部变量
举个例子:
function f() {
let value = Math.random();
function g() {
debugger; // 在 Console 中:输入 alert(value); No such variable!
}
return g;
}
let g = f();
g();
这不是调试器的 bug,而是 V8 的一个特别的特性。
老旧的 “var”
虽然我们在编程时不会使用 var
来声明变量,但是有一些老代码依然采用了这个写法,我们可能会需要阅读这些老代码
大部分情况下 var
和 let
没有明显的区别,但下面这些情况下会体现 var
的一些缺点或特性
1. “var” 没有块级作用域
var
声明的变量没有块级作用域的概念
举个例子:
if (true) {
var test = true; // 使用 "var" 而不是 "let"
}
alert(test); // true,变量在 if 结束后仍存在
但是 let
则不会,这是因为早期代码中块级作用域没有词法环境
2. “var” 允许重新声明
用 let
多次声明同一个变量会导致报错——重定义
但是 var
没有这个问题,新的声明语句会被忽略。
3. “var” 声明的变量可以在声明语句前被使用
当函数开始的时候,就会处理 var 声明,类似函数
举个例子:
function sayHi() {
phrase = "Hello";
alert(phrase);
var phrase;
}
sayHi();
就好像所有 var
声明都被自动 “提升” 到了代码顶部
但是请注意,var phrase = "Hello"
有两部分:声明 var phrase;
、赋值 phrase = "Hello
,只有 “声明” 的操作会被 “提升”,“赋值” 操作则不会
举个例子:
function sayHi() {
alert(phrase); // undefined
var phrase = "Hello";
}
sayHi();
等价于:
function sayHi() {
var phrase;
alert(phrase); // undefined
phrase = "Hello";
}
sayHi();
4. IIFE
立即调用函数表达式(immediately-invoked function expressions,IIFE),用于在老旧 Javascript 代码中模拟块级作用域
(function() {
var message = "Hello";
alert(message); // Hello
})();
也就是创建一个函数,然后立即调用他,因此,代码立即执行并拥有了自己的私有变量
我们需要用一些方法告诉引擎,我们在声明了这个函数之后立即调用了它:
(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");
}();
这些方法都可以
全局对象
全局对象提供可在任何地方使用的变量和函数,很多全局对象所提供的全局变量时内建的
在浏览器环境中全局对象被称为 window
,在 Node.js 中被称为 global
,而最新的标准支持 globalThis
这个名字,所有环境的全局对象都可以用它来表示:
alert("Hello");
// 等同于
window.alert("Hello");
// 等同于
globalThis.alert("Hello");
浏览器还有一个特点:用 var
声明的变量哦都会被当作全局变量被写入全局对象中
任意函数的函数声明也有同样的效果
var gVar = 5;
let gLet = 5;
alert(window.gVar); // 5(成为了全局对象的属性)
alert(window.gLet); // undefined(不会成为全局对象的属性)
我们也可以显式声明;
// 将当前用户信息全局化,以允许所有脚本访问它
window.currentUser = {
name: "John"
};
有些老旧的浏览器不存在现代浏览器拥有的一些全局对象的属性,我们可以利用这一点用于测试用户的浏览器环境是不是太老旧:
if (!window.Promise) {
alert("Your browser is really old!");
}
我们甚至可以借此直接填充这些对象:
if (!window.Promise) {
window.Promise = ... // 定制实现现代语言功能
}