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
  • 在条件测试中,nullundefined 都被视为假值。
  • 当需要表示变量没有值时,通常使用 null。当变量未初始化时,通常值是 undefined
  • 在访问对象的属性或数组的元素时,如果不存在,返回 undefined;如果显式将属性或元素值设置为 null,则会返回 null

函数提升与变量提升

函数提升:当使用 function 关键字声明函数时,整个函数声明会在代码执行之前被提升到当前作用域的顶部,并且在声明前还可以直接调用
注意:使用函数表达式声明的函数(通常是匿名函数)不会被提升,它们只能在声明之后调用。
变量提升:使用 var 关键字声明的变量,会被提升到当前作用域的顶部,但它们的值在声明前是 undefined

1
2
3
4
5
6
7
8
9
10
11
12
console.log(x); // 输出 undefined,变量声明提升,但值未初始化
var x = 10;

sayHello(); // 输出 "Hello!",函数声明提升,并且还可以调用
function sayHello() {
console.log("Hello!");
}

sayHello(); // 报错,sayHello 是 undefined
var sayHello = function() {
console.log("Hello!");
};
提升的优先级

在 JavaScript 中,函数提升的优先级高于变量提升,并且函数声明优先于变量声明。
这是因为 JavaScript 引擎在创建执行上下文时首先处理函数声明,然后再处理变量声明,确保函数在任何位置都可以被调用
若存在同一个函数/变量名,谁提升在前就用谁,后面相同的提升则被忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log(a); // ƒ a() {}

var a = 1;

function a() {}

console.log(a); // 1

// 上面代码等价于
function a() {}

var a // 变量名与前面的函数名相同,则被忽略了

console.log(a); // 打印函数本身,不是打印 undefined

a = 1

console.log(a); // 1,a 被重新赋值了

作用域链

是 JS 中用于查找变量的一种机制,由多个嵌套的作用域组成的链式结构,每个子作用域都能访问父作用域中的变量和函数,有助于确保变量的可见性和隔离不同作用域之间的变量。
查找机制:从子到父,最后到全局

上下文

JS 代码执行时,都会创建一个执行上下文,用于描述代码在运行时的环境和状态,它包含了当前代码执行所需的一切信息:如变量、函数、作用域链等。
创建执行上下文大概做的事情:

  • 初始化变量对象:它将包含该作用域中的所有变量和函数
  • 变量提升:将该作用域内的函数或某些变量的声明提升到顶部
  • 加入作用域链结构中
  • 放入调用栈内,后进先出(LIFO)策略

执行顺序

  1. 首次运行代码时,会创建全局执行上下文
  2. 执行代码:从全局作用域的第一行代码开始执行,逐行执行代码,包括函数调用
  3. 函数执行:遇到函数调用时,会新创建一个执行上下文,函数代码在其中执行
  4. 当遇到异步任务:定时器、网络请求等,会先将其加入事件队列中,等主任务执行完毕后,再通过事件循环检查事件队列,如果有任务待执行,就将它们取出并执行。当异步操作完成时,可以指定一个回调函数,在操作完成后执行回调函数。

this

定义

this 是一个特殊的关键字,它指向当前执行上下文中的对象,它的值取决于代码的上下文和执行方式

  • 全局上下文:this 指向全局对象(浏览器中为 window)
  • 函数中:
    • function 声明的函数
      • 普通函数:this 指向全局对象(浏览器中为 window)
      • 对象方法:this 指向该对象
    • 箭头函数:this 的值取决于定义函数时的上下文,而不是调用时的上下文,主要是继承包含它的父级函数或上下文的 this。
    • 构造函数:this 指向新创建的对象实例
    • 事件处理函数:this 指向触发事件的元素

如何改变 this 的指向?

call 和 apply

call、apply 是函数的方法,可将 this 做为传入的第一个值,并直接执行函数
funX.call(content, arg1, arg2, ……)
funX.apply(content, [arg1, arg2, ……])

1
2
3
4
5
6
function a() {
console.log(this.X)
}
const obj = { X: 1}
a.call(obj, 2, 3)
a.apply(obj, [2, 3])

bind

bind 是函数的方法,可将 this 做为传入的第一个值,并返回一个新的函数

1
2
3
4
5
6
7
function a(Y,Z) {
console.log(this.X)
console.log(Y)
console.log(Z)
}
const obj = { X: 1}
a.bind(obj)(2,3)

原理

  1. 创建一个新的函数,支持传参
  2. 该函数在调用时将 this 绑定为传入上下文

闭包

一句话:子函数使用了父函数变量,并在父函数外被使用,就形成了一个闭包
只要子函数存在,则其使用的父函数变量也将一直存在

作用

  • 保护/隐藏父函数的变量
    • 因为父函数的变量外部无法访问
  • 创建父函数的私有变量

场景

1
2
3
4
5
6
7
8
9
10
11
12
function email(){
const content = '我的信的内容'
return function(){
console.log(content)
}
}

let myEmail = email()

myEmail() // 能打印出 函数内部的 content 的值

myEmail = null // 这样能主动销毁闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 单一职责、高阶函数之类的
let content

function myEmail(fn){
content = '我是 XXX,打钱!'

fn()
}

function email(){
console.log(content)
}

myEmail(email) // 我是 XXX,打钱!

风险

  • 内存泄漏
    • 正常函数执行时创建变量,结束后销毁变量。但由于闭包的存在,只要子函数存在,则其使用的父函数变量也将一直存在
  • 性能问题

函数柯里化

将接受多个参数的函数,转化为嵌套的只接受单个参数的函数(使用闭包实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 未柯里化
function add(x,y,z){
return x + y + z
}
add(2, 3, 5)

// 柯里化
function add(x) {
return function(y){
return function(z){
return x + y + z
}
}
}
add(2)(3)(5)

补充知识

每个函数都有一个特殊的属性叫做 length。这个属性返回函数定义时的形参个数(即函数期望接收的参数个数)

1
2
3
4
function exampleFunction(a, b, c) {
// 函数体
}
exampleFunction.length // 3

通用柯里化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 通用柯里化函数
function curry(fn){
return function curried(...args){
if(args.length >= fn.length){
return fn(...args)
} else {
return function(...args2) {
return curried(...args.concat(args2));
};
}
}
}

// 加法函数
function add(a, b, c) {
return a + b + c;
}

var curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 输出 6
console.log(curriedAdd(1, 2)(3)); // 输出 6
console.log(curriedAdd(1)(2, 3)); // 输出 6
console.log(curriedAdd(1, 2, 3)); // 输出 6

柯里化的优点(ChatGPT 答案)

  1. 可组合性: 柯里化允许我们更容易地组合函数,创建新的函数,以满足不同的需求。
1
2
3
4
5
6
7
8
9
10
11
function greet(name) {
return "Hello, " + name + "!";
}

function uppercase(text) {
return text.toUpperCase();
}

var greetAndUppercase = curry(uppercase)(curry(greet)("Alice"));

console.log(greetAndUppercase()); // 输出 "HELLO, ALICE!"
  1. 参数复用: 柯里化允许我们重复使用相同的函数并提供不同的参数,这样可以减少代码重复。
  2. 延迟执行: 柯里化可以延迟函数的执行,直到接受了所有参数,这在某些情况下很有用。
  3. 函数的偏应用: 可以轻松实现函数的部分应用,即提供部分参数而不是所有参数,以创建新的函数。

柯里化在函数式编程中广泛应用,特别是在处理数据流、函数组合和函数式管道时,它可以帮助简化代码并提高可读性。

立即执行函数

立即执行函数(Immediately Invoked Function Expression,IIFE)是一种 JavaScript 中的函数表达式,它在声明后立即执行,是模块化的基石
这种模式常用于创建私有作用域、避免变量污染、模块化等场景。

形式

立即执行函数的基本形式如下

1
2
3
4
5
6
7
(function() {
// 这里是立即执行函数的代码块
})();

(function(param) {
console.log(param); // 输出 "Hello, World!"
})("Hello, World!");

在这个形式中,函数被包裹在括号内,这样解析器会将其视为一个表达式而不是函数声明。然后紧接着的一对括号 () 调用了这个匿名函数,使其立即执行。

作用

创建私有作用域

变量在立即执行函数内部声明,不会污染全局作用域

1
2
3
4
5
(function() {
var privateVariable = "I am private";
console.log(privateVariable); // 输出 "I am private"
})();
// console.log(privateVariable); // 这里会报错,privateVariable 不在全局作用域中

模块化

立即执行函数结合闭包,可以实现模块化的代码结构,提供了一种将变量和函数封装在独立作用域中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var myModule = (function() {
var privateVariable = "I am private";

function privateFunction() {
console.log("This is a private function");
}

return {
publicVariable: "I am public",
publicFunction: function() {
console.log("This is a public function");
}
};
})();

console.log(myModule.publicVariable); // 输出 "I am public"
myModule.publicFunction(); // 输出 "This is a public function"
// console.log(myModule.privateVariable); // 这里会报错,privateVariable 不可访问
// myModule.privateFunction(); // 这里会报错,privateFunction 不可访问

垃圾回收

JS 中的垃圾回收是一种自动管理内存的机制,它负责检测和回收不再使用的内存,以便释放资源。

触发时机

  1. 定期触发:会定期去触发垃圾回收
  2. 内存分配失败
    1. 当内存不足以分配新的对象时,会去触发垃圾回收

回收策略

  1. 标记-清除(常用)
    1. 标记:从全局对象开始遍历查找所有变量,第一遍都打上标记,第二遍去掉正在使用的变量的标记,这样被标记的就是不再使用的变量
    2. 清除:然后就专门清除被标记的变量,释放占用的内存引用计数
  2. 引用计数(不常用)
    1. 每当一个对象被引用时,引用计数加一,当引用失效时,计数减一。
    2. 当计数为零时,对象被视为不再使用,可以被回收。
    3. 然而,引用计数算法无法解决循环引用的问题,即两个或多个对象相互引用,但无法被外部访问,这导致它们的计数永远不会变为零。

解除引用

给变量设置为 null,就能解除引用,使其脱离执行环境,下次就能进行回收。

面试题

作用域 + 上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let a = 1

console.log(a)

function B(){
let b = 2
console.log(b)

C()
function C() {
let c = 3
console.log(c)

D()
function D() {
let d = 4
console.log(d)

console.log('test1', b)
}
}
}
B()

// 问题:打印结果是什么?

// 答案:
// 1
// 2
// 3
// 4
// test1 2

// 解析:
// 1、JS 的执行顺序是从上往下

this 面试题 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const foo = {
a: 1,
b: function(){
console.log(this.a)
console.log(this)
}
}
let c = foo.b
c()

// 问题:打印结果是什么?

// 答案:
// undefined
// Window 对象

this 面试题 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const o1 = {
text: "o1",
fun: function () {
console.log("[ this o1 fun ] >", this);
return this.text;
},
};

const o2 = {
text: "o2",
fun: function () {
return o1.fun();
},
};

const o3 = {
text: "o3",
fun: function () {
let fun = o1.fun;
return fun();
},
};
console.log("[ o1fun ] >", o1.fun());
console.log("[ o2fun ] >", o2.fun());
console.log("[ o3fun ] >", o3.fun());

// 问题 1:打印结果是什么?

// 答案:
// [ this o1 fun ] > {text: 'o1', fun: ƒ}
// [ o1fun ] > o1
// [ this o1 fun ] > {text: 'o1', fun: ƒ}
// [ o2fun ] > o1
// [ this o1 fun ] > Window {window: Window, self: Window, …}
// [ o3fun ] > undefined

// 问题2:如何将 o2.fun() 的返回结果改为 o2?

// 答案:
// const o2 = {
// text: "o2",
// fun: function () {
// return o1.fun.call(o2);
// },
// };

call/apply/bind 的区别

apply call bind
立即执行 返回新函数
参数为数组或类数组 参数为多个

手写一个 call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Function.prototype.MyCall = function (context, ...args) {
// 补齐相关代码
};

const fooX = function (X, Y) {
console.log(this.fo);
console.log(X);
console.log(Y);
};

fooX.MyCall({ fo: "fo" }, 1, 2);

// 答案如下:

Function.prototype.MyCall = function (context, ...args) {
// 补齐相关代码

context = context || window;

context.fn = this;

const result = context.fn(...args);

delete context.fn;

return result;
};

基于手写的 call 再手写一个 bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 手写的 call
Function.prototype.MyCall = function (context, ...args) {
context = context || window;

context.fn = this;

const result = context.fn(...args);

delete context.fn;

return result;
};

Function.prototype.MyBind = function(context) {
// 补齐相关代码
}

const fooX = function (X, Y) {
console.log(this.fo)
console.log(X)
console.log(Y)
}

fooX.MyBind({ fo: 'fo' }, 1, 2)()

// 答案如下:

Function.prototype.MyBind = function(context, ...args1){
// 补齐相关代码

const fun = this

return function (...args2) {
// 基于上面手写的 call
return fun.MyCall(context, args1.concat(args2))
}
}

1-1、作用域_上下文_this_闭包
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/1-1、作用域_上下文_this_闭包/
作者
黄智强
发布于
2024年1月13日
许可协议