作用域、this上下文、闭包

发布于:2025-02-10 ⋅ 阅读:(31) ⋅ 点赞:(0)

作用域(静态分析)

作用域是定义变量和访问变量的范围

作用域可以通过静态分析,不需要运行代码,就可以分析出当前作用域。

作用域分为:全局作用域、函数作用域、块级作用域。

  • 全局作用域:在顶层声明的变量和函数可以被访问的范围,通常就是在最外层(非函数体或循环体内)。全局变量中声明的变量和函数在任何地方都可以访问,包括其他作用域内部。在全局作用域中声明的变量,称为全局变量。
  • 函数作用域:函数作用域是在函数体内部声明的变量可以被访问的范围。在函数体外部无法被访问。函数作用域可以创建私有变量,这些被声明的变量只能在函数体中被访问。
  • 块级作用域:块级作用域是在代码块(if条件语句、for循环或大括号{}中的代码)内(let、const)声明的变量可以被访问的范围。在ES6(ECMAScript2015)之前,JavaScript中没有明确的块级作用域。可以使用var声明函数作用域变量。在ES6中引入let和const关键字后,当使用let和const声明变量时会产生块级作用域,变量的可访问范围在当前代码块内。

作用域规定了标识符(变量、函数等)在程序中的可见性生命周期。当变量被引用时,JavaScript引擎会根据作用域找到对应的变量。如果当前作用域中找不到该变量,就会继续往上级作用域中查找,直到找到或者到达全局作用域。这就形成了作用域链(这个过程被称为作用域链)(scope chain)。

作用域链

作用域链是变量和函数的可访问性和查找规则

function outer() {
    const outerVar = 'Outer variable';

    function inner() {
        const innerVar = 'Inner variable';
        console.log(innerVar); // Inner variable
        console.log(outerVar); // Outer variable
        console.log(globalVar); // Global variable
    }

    inner();
}

const globalVar = 'Global variable';
outer();

从上述代码可以看到,内部函数可以访问自身函数体内的变量,又可以访问外部函数体的变量,还能访问全局变量。作用域链会从内层,到外层,逐层查找。

如果不使用var声明变量呢?

console.log(d) //报错:is not defined  // 此时并没有发生变量提升
d=yunyin
// 'yunyin'
window.d
// 'yunyin'
function test() {
    e ='yy'
}
test()
console.log(e) // yy
console.log(window.e) // yy

可以看到,如果不用var进行变量声明,只做赋值操作,此时的变量会被挂在在window对象上。因此只有当变量使用var声明之后才会产生变量提升。 

函数提升

函数提升是默认存在的,目的是为了方便开发者,可以随意放置函数的位置。

变量提升

只有使用var声明的变量才会产生变量提升,只会提升变量定义,不会提升变量赋值。因此在变量声明之前使用var声明的变量会返回undefined(变量提升)。如果使用let、const声明的变量,在变量声明之前访问,会报错  Cannot access 'xx' before initialization。报错提示,无法在变量初始化之前访问。如:

console.log(dog)
let dog = 'wangcai'

 如果是在函数中定义的变量在外部访问,则会报错 Uncaught ReferenceError: dog is not defined。如下:

function funLog() {
    let dog = 'wangcai'
}
console.log(dog)

可以看到报错内容是不一样的。

this上下文(动态分析)

this是在执行时动态读取上下文决定的,而不是创建时决定的

函数直接调用——this指向window 

function foo(){
    console.log('函数内部', this)  //window对象
}
foo()

从上述代码可以看到,在函数体内的this指向window对象。这是因为当前被调用的foo函数,是在全局环境下被调用的。

隐式绑定——this执行调用堆栈的上级(对象、数组等引用关系逻辑)

function fn() {
    console.log('隐式绑定', this) //obj对象
}
const obj = {
    a: 1,
    fn
}
obj.fn = fn;
obj.fn();

从上述代码可以看出,当函数被obj对象引用后,obj对象调用函数时,this执行的是obj对象。说明当前函数作用域被调用时,指向被调用的对象。注意,引用不代表执行,this指向的是执行的对象,而不是引用对象。(当前函数是在哪里执行的)

猜猜this指向

const foo = {
    bar: 10,
    fn: function () {
        console.log(this.bar)
        console.log(this)
    }
}
const fn1 = foo.fn
fn1()

上述代码中,定义了fn1单独取出foo对象中的fn方法,此时定义的函数在windows上,因此this的上下文指向的是window,window上没有bar变量,所以为undefined

const o1 = {
    txt: 'o1',
    fn: function (){
        //直接使用上下文——传统派活
        console.log('o1fn', this)
        return this.txt
    }
}
const o2 = {
    txt: 'o2',
    fn: function (){
        //呼叫领导执行——部门协作
        return o1.fn()
    }
}
const o3 = {
     txt: 'o3',
     fn: function (){
        //直接内部构造——公共人
        let fn = o1.fn
        return fn()
     }
}
console.log('o1fn', o1.fn()); 
console.log('o2fn', o2.fn());
console.log('o3fn', o3.fn());

o1调用fn,此时this指向o1,在o2中只是返回了o1的调用,依然属于o1自身的调用,因此this是o1,o3把o1.fn重新抽出来赋给o3对象中的fn,返回定义的fn函数,此时调用fn与o3之间并无关联,fn是在window上执行的,而window上并没有text的值,因此返回undefined

如果需要将console.log('o2fn', o2.fn()) 的结果改成o2,如下:

//在o2初始化fn方法时,把o1的方法抽出,赋给o2方法
const o1 = {
    txt: 'o1',
    fn: function (){
        //直接使用上下文——传统派活
        console.log('o1fn', this)
        return this.txt
    }
}
const o2 = {
    txt: 'o2',
    fn: o1.fn
}
console.log('o2', o2.fn()) //this指向o2

显示绑定this

function foo() {
    console.log('函数内部', this);
}

foo();

// 使用
foo.call({
    a: 1
});
foo.apply({
    a: 1
});

const bindFoo = foo.bind({
    a: 1
});
bindFoo();

foo()属于函数调用此时this指向window,call、apply、bind调用后都让this指向了对象{a: 1}

call、apply、bind区别

(1)执行时机
  • call接收多个参数,第一个参数是this指向,从第二个参数开始是传递给函数的参数,call会立即执行函数
  • apply接收两个参数,第一个参数是this指向,第二个参数是一个数组,数组中的元素会被展开后传递给函数,apply也会立即执行函数
  • bind接收多个参数,第一个参数是this指向,第二个参数开始是传递给函数的参数。与call和apply不同,bind不会立即执行函数,而是返回一个新的函数,这个新的函数在调用时才会执行。
(2)参数传递方式
  • call和apply都可以传递数组或伪数组对象作为参数
  • apply的第二个参数必须数组或伪数组,而call和bind的参数是独立传递的
(3)使用场景
  • call适用于动态传递参数给函数的场景,特别是在不知道具体参数数量时
  • apply常用于需要传递多个参数给函数的场景,特别是参数较多时,使用apply可以避免手动展开参数的麻烦
  • bind适用于需要预先绑定this和部分参数,但不想立即执行函数的场景。bind返回的新函数可以在需要时调用,且可以保存绑定状态,直到实际需要执行时才使用。

bind实现

//1.需求:手写bind => bind位置(挂载在哪里) => Funtion.prototype 
Function.prototype.newBind = function(){
    const _this = this
    //2.bind是什么?
    const args = Array.prototype.slice.call(arguments)  //把伪数组转成数组
    // const args = [...arguments].slice(1) //把伪数组转成数组,并取出第一项
    // 输入:args特点,第一项是新this,第二项~最后一项函数传参
    const newThis = args.shift() //取出数组中的第一项
    // 返回:返回的是一个函数 =>构造一个函数 =>这个函数返回原函数的结果且继承传参
    return _this.newApply(newThis, args)
}
 
Function.prototype.newApply = function(context){
    //边缘检测
    if (typeof this !== 'function') {
        throw new TypeError('使用正确的函数进行调用') //如果当前调用方不是函数没法往下执行
    }
    //如果传入为空报错,context为新的上下文,如果没传默认在window上执行
    context = context || window
    // 执行函数的替换
    context.fn = this //指向当前执行的对象
    // 临时挂载指向fn => 销毁临时挂载
    let result = arguments[1] 
        ? context.fn(...arguments[1])
        : context.fn()
    
    delete context.fn
    //返回结果
    return result
}

闭包

function mail(){
    let content = 'mail'
    return function(){
        console.log(content)
    }
}
const envelop = mail()
envelop()
//局部变量content逃逸到了外部

闭包的含义:函数内部作用域以函数包裹的形式传递到外部,使内部作用域的变量逃逸到外部。

函数可以使JS产生封闭作用域,这JS模块的基石

闭包可以使模块返回变量,让JS真正实现模块化


网站公告

今日签到

点亮在社区的每一天
去签到