作用域
作用域
作用域:指的是变量的可见性和生命周期,即变量在代码中的访问范围和存在时间
大白话:它决定了变量的可访问性和有效期。
常见作用域
全局作用域
指变量在代码任何位置都可访问,即全局变量
浏览器端:指整个页面的范围
nodejs 端:指整个 Node.js 进程的范围
页面关闭或应用程序退出后,全局变量才失效
局部作用域
指变量只能在局部(函数或块级作用域)可访问,无法在外部访问
函数作用域
在函数内部声明的变量具有函数级作用域,只在函数执行期间有效,它们在函数被调用时创建,函数执行结束时销毁。
块级作用域
在 if 语句、for 循环的 {} 内部声明的变量具有块级作用域,只在块内执行期间有效,它们在块内执行时创建,块内结束时销毁。
作用域的作用
有助于避免变量名冲突和维护代码的可读性
常见声明
作用域常见报错
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
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 引擎在创建执行上下文时首先处理函数声明,然后再处理变量声明,确保函数在任何位置都可以被调用
若存在同一个函数/变量名,谁先声明就用谁
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)策略
执行顺序
- 首次运行代码时,会创建全局执行上下文
- 执行代码:从全局作用域的第一行代码开始执行,逐行执行代码,包括函数调用
- 函数执行:遇到函数调用时,会新创建一个执行上下文,函数代码在其中执行
- 当遇到异步任务:定时器、网络请求等,会先将其加入事件队列中,等主任务执行完毕后,再通过事件循环检查事件队列,如果有任务待执行,就将它们取出并执行。当异步操作完成时,可以指定一个回调函数,在操作完成后执行回调函数。
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, ....])
function a() {
console.log(this.X)
}
const obj = { X: 1}
a.call(obj, 2, 3)
a.apply(obj, [2, 3])
bind
bind 是函数的方法,可将 this 做为传入的第一个值,并返回一个新的函数
function a(Y,Z) {
console.log(this.X)
console.log(Y)
console.log(Z)
}
const obj = { X: 1}
a.bind(obj)(2,3)
原理
- 创建一个新的函数,支持传参
- 该函数在调用时将 this 绑定为传入上下文
闭包
一句话:子函数使用了父函数变量,并在父函数外被使用,就形成了一个闭包
只要子函数存在,则其使用的父函数变量也将一直存在
作用
- 保护/隐藏父函数的变量
-
- 因为父函数的变量外部无法访问
- 创建父函数的私有变量
场景
function email(){
const content = '我的信的内容'
return function(){
console.log(content)
}
}
let myEmail = email()
myEmail() // 能打印出 函数内部的 content 的值
myEmail = null // 这样能主动销毁闭包
// 单一职责、高阶函数之类的
let content
function myEmail(fn){
content = '我是 XXX,打钱!'
fn()
}
function email(){
console.log(content)
}
myEmail(email) // 我是 XXX,打钱!
风险
- 内存泄漏
-
- 正常函数执行时创建变量,结束后销毁变量。但由于闭包的存在,只要子函数存在,则其使用的父函数变量也将一直存在
- 性能问题
函数柯里化
将接受多个参数的函数,转化为嵌套的只接受单个参数的函数(使用闭包实现)
// 未柯里化
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。这个属性返回函数定义时的形参个数(即函数期望接收的参数个数)
function exampleFunction(a, b, c) {
// 函数体
}
exampleFunction.length // 3
通用柯里化函数
// 通用柯里化函数
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 答案):
- 可组合性: 柯里化允许我们更容易地组合函数,创建新的函数,以满足不同的需求。
function greet(name) {
return "Hello, " + name + "!";
}
function uppercase(text) {
return text.toUpperCase();
}
var greetAndUppercase = curry(uppercase)(curry(greet)("Alice"));
console.log(greetAndUppercase()); // 输出 "HELLO, ALICE!"
- 参数复用: 柯里化允许我们重复使用相同的函数并提供不同的参数,这样可以减少代码重复。
- 延迟执行: 柯里化可以延迟函数的执行,直到接受了所有参数,这在某些情况下很有用。
- 函数的偏应用: 可以轻松实现函数的部分应用,即提供部分参数而不是所有参数,以创建新的函数。
柯里化在函数式编程中广泛应用,特别是在处理数据流、函数组合和函数式管道时,它可以帮助简化代码并提高可读性。
立即执行函数
立即执行函数(Immediately Invoked Function Expression,IIFE)是一种 JavaScript 中的函数表达式,它在声明后立即执行,是模块化的基石
这种模式常用于创建私有作用域、避免变量污染、模块化等场景。
形式
立即执行函数的基本形式如下
(function() {
// 这里是立即执行函数的代码块
})();
(function(param) {
console.log(param); // 输出 "Hello, World!"
})("Hello, World!");
在这个形式中,函数被包裹在括号内,这样解析器会将其视为一个表达式而不是函数声明。然后紧接着的一对括号 () 调用了这个匿名函数,使其立即执行。
作用
创建私有作用域
变量在立即执行函数内部声明,不会污染全局作用域
(function() {
var privateVariable = "I am private";
console.log(privateVariable); // 输出 "I am private"
})();
// console.log(privateVariable); // 这里会报错,privateVariable 不在全局作用域中
模块化
立即执行函数结合闭包,可以实现模块化的代码结构,提供了一种将变量和函数封装在独立作用域中的方法。
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 中的垃圾回收是一种自动管理内存的机制,它负责检测和回收不再使用的内存,以便释放资源。
触发时机
- 定期触发:会定期去触发垃圾回收
- 内存分配失败
-
- 当内存不足以分配新的对象时,会去触发垃圾回收
回收策略
- 标记-清除(常用)
-
- 标记:从全局对象开始遍历查找所有变量,第一遍都打上标记,第二遍去掉正在使用的变量的标记,这样被标记的就是不再使用的变量
- 清除:然后就专门清除被标记的变量,释放占用的内存引用计数
- 引用计数(不常用)
-
- 每当一个对象被引用时,引用计数加一,当引用失效时,计数减一。
- 当计数为零时,对象被视为不再使用,可以被回收。
- 然而,引用计数算法无法解决循环引用的问题,即两个或多个对象相互引用,但无法被外部访问,这导致它们的计数永远不会变为零。
解除引用
给变量设置为 null,就能解除引用,使其脱离执行环境,下次就能进行回收。
面试题
作用域 + 上下文
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
const foo = {
a: 1,
b: function(){
console.log(this.a)
console.log(this)
}
}
let c = foo.b
c()
// 问题:打印结果是什么?
// 答案:
// undefined
// Window 对象
this 面试题 2
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 的区别
手写一个 call
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
// 手写的 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))
}
}