Skip to content

JS 基础语法

变量、函数声明

INFO

尽管现在没人用 var 了,但是还是有人问

var 没有块级作用域

js
// 全局作用域
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)

js
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

总结:

  1. 只有 var 可以重复声明同名变量而不报错,而先 var 后 let 或者先 let 后 var 都不行
  2. 函数提升优先级高,不会被变量声明覆盖(即使它更靠后)
  3. 匿名函数不提升(如:声明后赋值给变量)

IIFE 立即调用函数表达式

js
// 常见
(function () {
    // do something
})();

// 常见
(function () {
    // do something
}());

// 不常见
!function () {
    // do something
}();

// 不常见
+function () {
    // do something
}();

TIP

总结: IIFE 是为了解决 var 没有块级作用域问题的(模仿)

变量的挂载

js
var name = 'nickel'
console.log(global.name) // node env
console.log(window.name) // web env

var 声明的变量 无论是 web 环境还是 node 环境,在交互式控制台用 var 声明变量都会挂载在 window 或 global 下, 而如果采用文件的执行方式,此时浏览器环境下的输出不变而 node 环境下的值会变成 undefined.

什么是文件执行方式?

html

<html lang="zh-CN">
<head>
    <script src="index.js"></script>
    <title></title>
</head>
</html>
js
var name = 'nickel'
console.log(name)
console.log(window.name) // web env

INFO

测试环境:
NodeJS: v18.12.1
Chrome: 109.0.5414.120 (正式版本)

为什么会是 undefinedname 到底去哪里了?
name 其实被挂载到了一个特殊的块级作用域内,如果使用 webstorm 的调试功能,可以清晰地看到, name 并不在 global 选项卡中,而是在 “本地” 选项卡中,这里的本地应该指的是这个文件的 “根上下文“

TIP

类似地,let 和 const 声明的变量也被放在了所谓的 ”根上下文“,所以不会污染 global

let 和 const

因为 let 和 const 平时用的很多了,对它们的特性比较熟悉,所以只是列出而不解释

  1. 块级作用域
  2. 暂时性死区
  3. 不可重复声明
  4. const 声明的变量不可更改(基础类型),地址不可更改(引用类型)

闭包(closure)

不同的定义和说法

MDN

⚠ 面试回答背这个

⭐ 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。 在 JavaScript 中,闭包会随着函数的创建而被同时创建。

现代 JavaScript 教程
闭包是指一个函数可以记住其外部变量并可以访问这些变量。

⭐ 在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是闭包的定义, 并解释清楚为什么 JavaScript 中的所有函数都是闭包的,以及可能的关于 [[Environment]] 属性和词法环境原理的技术细节

维基百科
闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。

实现细节

在 JavaScript 中,每个函数、代码块、整个脚本,都有一个名为 “词法环境” 的内部关联对象, 它包含下面两种属性:

  • 环境记录(存储所有局部变量,this 值等属性)
  • 外部词法环境的引用

对于函数来讲,在自身被创建时,内部会有一个 [[Environment]] 或者 [[Scope]] 隐藏对象保存着当前的词法环境; 当函数被调用时,会创建一个新的词法环境对象(道理如同 Vue 中 data 必须是函数而不是对象)

js
function sayName() {
    let name = 'nickel'
    return function (newName) {
        name = newName
        console.log(name)
    }
}

上面的代码显然是一个常见的闭包情景,函数 sayName 在创建时保存内部名为 name 的属性到 [[Environment]] 中, 然后创建匿名函数,此匿名函数保存了对外部词法环境 name 的引用,所以不论创建了多少该匿名函数,每次修改的都是同一个变量

js
let func1 = sayName()
let func2 = sayName()

func1('nanote')
func2('nana')

// name: nana

例外情况

如果我们使用 new Function 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境。

因为考虑到代码压缩,大部分变量会被替换为 a, b, c 这种简洁的形式,所以指向全局是可靠的

js
let num = 100;
let func = new Function('console.log(num)')

func()  // ReferenceError: a is not defined

global.a = 100

func()  // 100
js
// 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