作用域&闭包

167 阅读6分钟

一、var 、let、const

1. 块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,letconst为 JavaScript 新增块级作用域。

2. 重复声明

letconst不允许在相同作用域内,重复声明同一个变量。var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的变量。

3. 变量提升

var命令会变量提升,即变量可以在声明之前使用,值为undefined

letconst命令声明的变量一定要在声明后使用,否则报错ReferenceError

function声明的函数也会进行提升,优先级高于变量提升,不会被变量声明所覆盖,但是会被变量赋值后覆盖。

console.log(a) // function a(params) {}
var a =1
function a(params) {}
console.log(a) //1

4. 暂时性死区

在代码块内,使用letconst命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

ES6 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

5. 全局变量

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

var命令和function命令声明的全局变量,依旧是顶层对象的属性; let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

var a = 1;      
// Node环境可以写成global.a
// 或者通用写法,this.a
// 或者顶层对象写法 globalThis.a

window.a        // 1
this.a             // 1
globalThis.a    // 1

let b = 1;
window.b     // undefined
this.a          // undefined
globalThis.a // undefined

ES2020 在语言标准的层面,引入globalThis作为顶层对象。任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this

6. 指针指向

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

7. 初始值设置

对于const来说,只声明不赋值,就会报错.

const foo;
// SyntaxError: Missing initializer in const declaration

二、作用域

1. 全局作用域和函数作用域

(1)全局作用域

1.最外层函数和变量拥有全局作用域;

2.所有未定义直接赋值的变量自动声明为全局作用域;

3所有window对象的属性拥有全局作用域。

(2)函数作用域

1.子函数可以访问父函数中的变量;

2.父函数无法访问子函数中的变量。

2. 块级作用域

ES6中新增了let和const命令,可以声明块级作用域。

作用域链

作用域链的作用:保证执行环境有权访问所有变量和函数的有序访问,通过作用域链,可以访问到外层环境变量和函数。

三、闭包

闭包:指有权访问另一个函数作用域中变量的函数。

function f1() {
    var n=999;
    function f2() {
        console.log(n);
        return n
    }
    return f2;
}
var result=f1();
var num= result(); // 999

闭包作用:

1.保存:可以在外部调用闭包函数,从而在外部访问到函数内部的变量,但会造成内存泄漏; 2.保护:闭包内部变量不会被外部随意修改,从而实现封装,模块化。

四、this

this总是指向函数的直接调用者(而非间接调用者)。

1. 全局环境

全局环境中,this会返回全局对象。

2. 函数环境

函数环境中的this,如果函数不是作为方法调用,而是单纯作为函数调用,this会返回全局对象。

3. 方法环境

方法环境中的this,指向该方法所在的对象。

4. 构造函数环境

构造函数中的this,指向返回的实例对象。

5. 箭头函数环境

箭头函数没有自己的this,箭头函数的this,只取决于外层代码块的this。

五、call、apply、bind

1. call

调用一个对象的一个方法,以另一个对象替换当前对象。

var obj1 = {
    name: 'obj1',
    foo1: function () {
        console.log(this.name);
    }
};
var obj2 = {
    name: 'obj2'
};
obj1.foo1.call(obj2); // obj2

2. apply

调用一个对象的一个方法,以另一个对象替换当前对象。

var obj1 = {
    name: 'obj1',
    foo1: function () {
        console.log(this.name);
    }
};
var obj2 = {
    name: 'obj2'
};
obj1.foo1.apply(obj2); // obj2

3. bind

创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其参数则来自于 bind() 之后传入的参数。

var obj1 = {
    name: 'obj1',
    foo1: function () {
        console.log(this.name);
    }
};
var obj2 = {
    name: 'obj2'
};
var foo1 = obj1.foo1.bind(obj2);
foo1(); // obj2

4. 区别

相同点:

  1. 都可以改变 this 的指向;
  2. 第一个参数都是this要指向的对象。

不同点:

  1. call 和 apply 会调用函数, bind 不会;
  2. call 和 apply 传递参数的方式不一样,call 传递参数比较直接,apply 需要以数组的形式进行传递;
  3. bind 除了改变 this 指向之外,它返回的也是一个函数。

5. call、apply、bind实现

Function.prototype.myCall = function (ctx, ...args) {
    // console.log(this); // [Function: method] this就是method函数
    // ctx.__fn = this   // this赋值给__fn函数,
    // const result =ctx.__fn(...args) // 剩余参数传递给method函数
    // delete ctx.__fn  // 删除方法
    // return result

    // 判断调用对象是否为函数
    if (typeof this !== 'function') {
        throw error(`TypeError:methods is not a function!`)
    }

    // 若传递的为null和undefined 指向globalThis,非对象转换为对象
    ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
    
    // __fn有可能重复所以使用symbol创建属性
    const key = Symbol()
    
    // 设置可枚举属性让其不会出现在ctx中进行遍历
    Reflect.defineProperty(ctx, key, {
        value: this,
        enumerable: false,
        writable: true,
        configurable: true
    })
    const result = ctx[key](...args)
    // Reflect.deleteProperty(ctx, key) // 此方法可以不用
    return result
}

Function.prototype.myApply = function (ctx, args) {
    if (typeof this !== 'function') {
        throw error(`TypeError:methods is not a function!`)
    }
    ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
    const key = Symbol()
    Reflect.defineProperty(ctx, key, {
        value: this,
        enumerable: false,
        writable: true,
        configurable: true
    })
    const result = ctx[key](...args)
    Reflect.deleteProperty(ctx, key)
    return result
}

Function.prototype.myBind = function (ctx, ...args) {
    if (typeof this !== 'function') {
        throw error(`TypeError:methods is not a function!`)
    }
    let fn = this
    return function (...args2) {
        return fn.call(ctx, ...args, ...args2)
    }
}

function method(a, b) {
    console.log(this, a, b);
    return a + b
}
method.call({}, 2, 3)
method.myCall({}, 2, 3)
    (1).myCall({}, 2)

六、箭头函数

箭头函数没有自己的 this,箭头函数的 this 只取决于外层代码块的 this。

var name='11111'
var obj1 = {
    name: 'obj1',
    foo1: function () {
        console.log(this.name);
    },
    foo2: () => console.log(this.name),
    foo3: function () {
        return function () {
            console.log(this.name);
        }
    },
    foo4: function () {
        return () => {
            console.log(this.name);
        }
    }
};
obj1.foo1(); // obj1
obj1.foo2(); // 11111,指向window
obj1.foo3()(); // 11111,this指向window
obj1.foo4()(); // obj1