1-1 作用域/上下文/this/闭包

145 阅读11分钟

作用域

作用域

作用域:指的是变量的可见性和生命周期,即变量在代码中的访问范围和存在时间

大白话:它决定了变量的可访问性和有效期。

常见作用域

全局作用域

指变量在代码任何位置都可访问,即全局变量

浏览器端:指整个页面的范围

nodejs 端:指整个 Node.js 进程的范围

页面关闭或应用程序退出后,全局变量才失效

局部作用域

指变量只能在局部(函数或块级作用域)可访问,无法在外部访问

函数作用域

在函数内部声明的变量具有函数级作用域,只在函数执行期间有效,它们在函数被调用时创建,函数执行结束时销毁。

块级作用域

在 if 语句、for 循环的 {} 内部声明的变量具有块级作用域,只在块内执行期间有效,它们在块内执行时创建,块内结束时销毁。

作用域的作用

有助于避免变量名冲突和维护代码的可读性

常见声明

image.png

作用域常见报错

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)策略

执行顺序

  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, ....])

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)

原理

  1. 创建一个新的函数,支持传参
  2. 该函数在调用时将 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 答案):

  1. 可组合性: 柯里化允许我们更容易地组合函数,创建新的函数,以满足不同的需求。
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 中的函数表达式,它在声明后立即执行,是模块化的基石

这种模式常用于创建私有作用域、避免变量污染、模块化等场景。

形式

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

(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 中的垃圾回收是一种自动管理内存的机制,它负责检测和回收不再使用的内存,以便释放资源。

触发时机

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

回收策略

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

解除引用

给变量设置为 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 的区别

image.png

手写一个 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))
  }
}