1-1、作用域_上下文_this_闭包
作用域
作用域
作用域:指的是变量的可见性和生命周期,即变量在代码中的访问范围和存在时间
大白话:它决定了变量的可访问性和有效期。
常见作用域
全局作用域
指变量在代码任何位置都可访问,即全局变量
浏览器端:指整个页面的范围
nodejs 端:指整个 Node.js 进程的范围
页面关闭或应用程序退出后,全局变量才失效
局部作用域
指变量只能在局部(函数或块级作用域)可访问,无法在外部访问
函数作用域
在函数内部声明的变量具有函数级作用域,只在函数执行期间有效,它们在函数被调用时创建,函数执行结束时销毁。
块级作用域
在 if 语句、for 循环的 {} 内部声明的变量具有块级作用域,只在块内执行期间有效,它们在块内执行时创建,块内结束时销毁。
作用域的作用
有助于避免变量名冲突和维护代码的可读性
常见声明
var | let | const |
---|---|---|
作用域为全局、函数,无块级 | 作用域始终为块级 | |
具有变量提升 | ||
console.log(a) | ||
var a = 1 | ||
(上述代码不会引发错误) | ||
等价于 | ||
var a = undefined | ||
console.log(a) | ||
a = 1 | 不具有变量提升 | |
console.log(a) | ||
let a = 1 | ||
(上述代码报错 ReferenceError: Cannot access ‘a’ before initialization) | ||
可重复声明 | 不可重复声明,会报错 Identifier ‘x’ has already been declared | |
声明时可不赋值,后续可改值 | 声明时必须赋值,后续不可改值 |
作用域常见报错
ReferenceError: x is not defined
访问未定义的变量时报错,原因是:变量没有在当前作用域声明或在声明之前访问
TypeError: Cannot read property ‘property’ of null
访问变量的属性或方法时报错,原因是:变量本身为 null 或 undefined
其他补充知识
变量初始化
两步:声明变量、并为变量赋予一个初始值。未赋予初始值的变量值会默认为 undefined
let a = 7; // 初始化了
var b; // 未初始化,a 为 undefined
null 和 undefined
- undefined 表示声明了但未赋值或缺失值,通常由 JavaScript 引擎自动生成的。
- null 表示声明了并赋值为 null
- 在条件测试中,null 和 undefined 都被视为假值。
- 当需要表示变量没有值时,通常使用 null。当变量未初始化时,通常值是 undefined。
- 在访问对象的属性或数组的元素时,如果不存在,返回 undefined;如果显式将属性或元素值设置为 null,则会返回 null。
函数提升与变量提升
函数提升:当使用 function 关键字声明函数时,整个函数声明会在代码执行之前被提升到当前作用域的顶部,并且在声明前还可以直接调用
注意:使用函数表达式声明的函数(通常是匿名函数)不会被提升,它们只能在声明之后调用。
变量提升:使用 var 关键字声明的变量,会被提升到当前作用域的顶部,但它们的值在声明前是 undefined
1 |
|
提升的优先级
在 JavaScript 中,函数提升的优先级高于变量提升,并且函数声明优先于变量声明。
这是因为 JavaScript 引擎在创建执行上下文时首先处理函数声明,然后再处理变量声明,确保函数在任何位置都可以被调用
若存在同一个函数/变量名,谁提升在前就用谁,后面相同的提升则被忽略
1 |
|
作用域链
是 JS 中用于查找变量的一种机制,由多个嵌套的作用域组成的链式结构,每个子作用域都能访问父作用域中的变量和函数,有助于确保变量的可见性和隔离不同作用域之间的变量。
查找机制:从子到父,最后到全局
上下文
JS 代码执行时,都会创建一个执行上下文,用于描述代码在运行时的环境和状态,它包含了当前代码执行所需的一切信息:如变量、函数、作用域链等。
创建执行上下文大概做的事情:
- 初始化变量对象:它将包含该作用域中的所有变量和函数
- 变量提升:将该作用域内的函数或某些变量的声明提升到顶部
- 加入作用域链结构中
- 放入调用栈内,后进先出(LIFO)策略
执行顺序
- 首次运行代码时,会创建全局执行上下文
- 执行代码:从全局作用域的第一行代码开始执行,逐行执行代码,包括函数调用
- 函数执行:遇到函数调用时,会新创建一个执行上下文,函数代码在其中执行
- 当遇到异步任务:定时器、网络请求等,会先将其加入事件队列中,等主任务执行完毕后,再通过事件循环检查事件队列,如果有任务待执行,就将它们取出并执行。当异步操作完成时,可以指定一个回调函数,在操作完成后执行回调函数。
this
定义
this 是一个特殊的关键字,它指向当前执行上下文中的对象,它的值取决于代码的上下文和执行方式
- 全局上下文:this 指向全局对象(浏览器中为 window)
- 函数中:
- function 声明的函数
- 普通函数:this 指向全局对象(浏览器中为 window)
- 对象方法:this 指向该对象
- 箭头函数:this 的值取决于定义函数时的上下文,而不是调用时的上下文,主要是继承包含它的父级函数或上下文的 this。
- 构造函数:this 指向新创建的对象实例
- 事件处理函数:this 指向触发事件的元素
- function 声明的函数
如何改变 this 的指向?
call 和 apply
call、apply 是函数的方法,可将 this 做为传入的第一个值,并直接执行函数
funX.call(content, arg1, arg2, ……)
funX.apply(content, [arg1, arg2, ……])
1 |
|
bind
bind 是函数的方法,可将 this 做为传入的第一个值,并返回一个新的函数
1 |
|
原理
- 创建一个新的函数,支持传参
- 该函数在调用时将 this 绑定为传入上下文
闭包
一句话:子函数使用了父函数变量,并在父函数外被使用,就形成了一个闭包
只要子函数存在,则其使用的父函数变量也将一直存在
作用
- 保护/隐藏父函数的变量
- 因为父函数的变量外部无法访问
- 创建父函数的私有变量
场景
1 |
|
1 |
|
风险
- 内存泄漏
- 正常函数执行时创建变量,结束后销毁变量。但由于闭包的存在,只要子函数存在,则其使用的父函数变量也将一直存在
- 性能问题
函数柯里化
将接受多个参数的函数,转化为嵌套的只接受单个参数的函数(使用闭包实现)
1 |
|
补充知识
每个函数都有一个特殊的属性叫做 length。这个属性返回函数定义时的形参个数(即函数期望接收的参数个数)
1 |
|
通用柯里化函数
1 |
|
柯里化的优点(ChatGPT 答案)
- 可组合性: 柯里化允许我们更容易地组合函数,创建新的函数,以满足不同的需求。
1 |
|
- 参数复用: 柯里化允许我们重复使用相同的函数并提供不同的参数,这样可以减少代码重复。
- 延迟执行: 柯里化可以延迟函数的执行,直到接受了所有参数,这在某些情况下很有用。
- 函数的偏应用: 可以轻松实现函数的部分应用,即提供部分参数而不是所有参数,以创建新的函数。
柯里化在函数式编程中广泛应用,特别是在处理数据流、函数组合和函数式管道时,它可以帮助简化代码并提高可读性。
立即执行函数
立即执行函数(Immediately Invoked Function Expression,IIFE)是一种 JavaScript 中的函数表达式,它在声明后立即执行,是模块化的基石
这种模式常用于创建私有作用域、避免变量污染、模块化等场景。
形式
立即执行函数的基本形式如下
1 |
|
在这个形式中,函数被包裹在括号内,这样解析器会将其视为一个表达式而不是函数声明。然后紧接着的一对括号 () 调用了这个匿名函数,使其立即执行。
作用
创建私有作用域
变量在立即执行函数内部声明,不会污染全局作用域
1 |
|
模块化
立即执行函数结合闭包,可以实现模块化的代码结构,提供了一种将变量和函数封装在独立作用域中的方法。
1 |
|
垃圾回收
JS 中的垃圾回收是一种自动管理内存的机制,它负责检测和回收不再使用的内存,以便释放资源。
触发时机
- 定期触发:会定期去触发垃圾回收
- 内存分配失败
- 当内存不足以分配新的对象时,会去触发垃圾回收
回收策略
- 标记-清除(常用)
- 标记:从全局对象开始遍历查找所有变量,第一遍都打上标记,第二遍去掉正在使用的变量的标记,这样被标记的就是不再使用的变量
- 清除:然后就专门清除被标记的变量,释放占用的内存引用计数
- 引用计数(不常用)
- 每当一个对象被引用时,引用计数加一,当引用失效时,计数减一。
- 当计数为零时,对象被视为不再使用,可以被回收。
- 然而,引用计数算法无法解决循环引用的问题,即两个或多个对象相互引用,但无法被外部访问,这导致它们的计数永远不会变为零。
解除引用
给变量设置为 null,就能解除引用,使其脱离执行环境,下次就能进行回收。
面试题
作用域 + 上下文
1 |
|
this 面试题 1
1 |
|
this 面试题 2
1 |
|
call/apply/bind 的区别
apply | call | bind |
---|---|---|
立即执行 | 返回新函数 | |
参数为数组或类数组 | 参数为多个 |
手写一个 call
1 |
|
基于手写的 call 再手写一个 bind
1 |
|