JS 基础语法
变量、函数声明
INFO
尽管现在没人用 var 了,但是还是有人问
var
没有块级作用域
// 全局作用域
if (true) {
if (true) {
var name = 'nickel'
}
}
console.log(name) // 'nickel'
// 函数作用域
function test() {
var age = 18
}
console.log(age) // ReferenceError: age is not defined
TIP
总结:var
声明的变量只有函数作用域和全局作用域,没有块级作用域
提升(hoisting,raising)
console.log(name) // undefined
var name = 'nickel'
getName() // nickel
function getName() {
console.log('nickel')
}
var getAge
var getName = null
getAge() // 18,并不会报错
function getAge() {
console.log(18)
}
TIP
总结:
- 只有 var 可以重复声明同名变量而不报错,而先 var 后 let 或者先 let 后 var 都不行
- 函数提升优先级高,不会被变量声明覆盖(即使它更靠后)
- 匿名函数不提升(如:声明后赋值给变量)
IIFE 立即调用函数表达式
// 常见
(function () {
// do something
})();
// 常见
(function () {
// do something
}());
// 不常见
!function () {
// do something
}();
// 不常见
+function () {
// do something
}();
TIP
总结: IIFE 是为了解决 var 没有块级作用域问题的(模仿)
变量的挂载
var name = 'nickel'
console.log(global.name) // node env
console.log(window.name) // web env
var
声明的变量 无论是 web 环境还是 node 环境,在交互式控制台用 var 声明变量都会挂载在 window 或 global 下, 而如果采用文件的执行方式,此时浏览器环境下的输出不变而 node 环境下的值会变成 undefined.
什么是文件执行方式?
<html lang="zh-CN">
<head>
<script src="index.js"></script>
<title></title>
</head>
</html>
var name = 'nickel'
console.log(name)
console.log(window.name) // web env
INFO
测试环境:
NodeJS: v18.12.1
Chrome: 109.0.5414.120 (正式版本)
为什么会是 undefined
,name
到底去哪里了?
name 其实被挂载到了一个特殊的块级作用域内,如果使用 webstorm
的调试功能,可以清晰地看到, name 并不在 global 选项卡中,而是在 “本地” 选项卡中,这里的本地应该指的是这个文件的 “根上下文“
TIP
类似地,let 和 const 声明的变量也被放在了所谓的 ”根上下文“,所以不会污染 global
let 和 const
因为 let 和 const 平时用的很多了,对它们的特性比较熟悉,所以只是列出而不解释
- 块级作用域
- 暂时性死区
- 不可重复声明
- const 声明的变量不可更改(基础类型),地址不可更改(引用类型)
闭包(closure)
不同的定义和说法
⚠ 面试回答背这个
⭐ 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。 在 JavaScript 中,闭包会随着函数的创建而被同时创建。
现代 JavaScript 教程
闭包是指一个函数可以记住其外部变量并可以访问这些变量。
⭐ 在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是
闭包的定义
, 并解释清楚为什么 JavaScript 中的所有函数都是闭包的
,以及可能的关于 [[Environment]] 属性和词法环境原理的技术细节
。
维基百科
闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
实现细节
在 JavaScript 中,每个函数、代码块、整个脚本,都有一个名为 “词法环境” 的内部关联对象, 它包含下面两种属性:
- 环境记录(存储所有局部变量,this 值等属性)
- 外部词法环境的引用
对于函数来讲,在自身被创建时,内部会有一个 [[Environment]]
或者 [[Scope]]
隐藏对象保存着当前的词法环境; 当函数被调用时,会创建一个新的词法环境对象(道理如同 Vue 中 data 必须是函数而不是对象)
function sayName() {
let name = 'nickel'
return function (newName) {
name = newName
console.log(name)
}
}
上面的代码显然是一个常见的闭包情景,函数 sayName
在创建时保存内部名为 name
的属性到 [[Environment]]
中, 然后创建匿名函数,此匿名函数保存了对外部词法环境 name
的引用,所以不论创建了多少该匿名函数,每次修改的都是同一个变量
let func1 = sayName()
let func2 = sayName()
func1('nanote')
func2('nana')
// name: nana
例外情况
如果我们使用 new Function
创建一个函数,那么该函数的 [[Environment]]
并不指向当前的词法环境,而是指向全局环境。
因为考虑到代码压缩,大部分变量会被替换为 a, b, c
这种简洁的形式,所以指向全局是可靠的
let num = 100;
let func = new Function('console.log(num)')
func() // ReferenceError: a is not defined
global.a = 100
func() // 100
// var a = 100
// global.a = 100
function hello() {
let a = 100;
let func = new Function('console.log(a)')
func()
// ReferenceError: a is not defined
// 100
}
hello()
说明
这里所指的 “全局” 不是脚本的 root
,而是 global
或者 window