this&闭包&作用域

134 阅读9分钟

this

this可以认为是函数运行的上下文,可以理解为一个动态的对象,普通函数中的this在调用时才确定指向。

this使得函数的复用可以使用不同的上下文,通过不同的this调用同一个函数可以得到不同的结果。

this的绑定规则

  1. 默认绑定

    • 非严格模式下 this 指向全局对象(浏览器指向window,Node环境指向Global);
    • 严格模式下,this 绑定到undefined,严格模式不允许 this 绑定到全局对象。
    var a = 'hello';
    var obj = {
        a:'js',
        foo:function(){
            //'use strict';
            console.log(this.a);
        }
    }
    var bar = obj.foo;
    bar()//hello
    

    上述代码在浏览器环境执行,严格模式和非严格模式下得到结果不同

    • 非严格模式下会输出hello;
    • 严格模式会报错,Uncaught TypeError: Cannot read properties of undefined (reading 'a')

    注意:普通函数在作为参数传递时(setTimeout、setInterval),执行在非严格模式下this指向全局对象

    var a = 10;
    var obj = {
        a:20,
        foo:function(){
            setTimeout(function(){
                console.log("hello",this.a)
            })
        }
    }
    obj.foo()//hello 10
    
  2. 隐式绑定 与默认绑定相反,函数调用的时候有显式的修饰符,如xxx.foo()。
    下面的代码,foo方法作为obj方法是作为对象的属性调用的,此时foo方法执行时的this指向obj。

    var a = 10;
    var obj = {
        a:20,
        foo:function(){
            console.log(this.a);
        }
    }
    obj.foo();//20
    

    注意:链式调用的情况下,this会就近指向

    function sayHi(){
        console.log('嗨',this.name);
    }
    var person1 = {
        name:'tom',
        foo:sayHi,
    }
    var person2 = {
        name:'jam',
        friend:person1,
    }
    person2.friend.foo();//嗨 tom
    
  3. 显示绑定 通过函数call、apply和bind可以修改函数this的指向

    call和apply
    • callapply的第一个参数都会绑定到函数体的this上,如果不传参func.call(),非严格模式下this默认绑定到全局对象
    • call函数的参数是一个个增加,而apply第二个参数接收一个数组
    func.call(this,arg1,arg2,...);
    func.apply(this,[arg1,arg2,...]);
    
    var animal = {
        name:'aa'
    }
    function addProp(type,color){
        this.color = color;
        this.type = type;
    }
    
    addProp.call(animal,'dog','white');
    console.log(animal.color);//white
    
    addProp.apply(animal,['cat','black']);
    console.log(animal.color);//black
    

    注意:如果我们调用call或者apply时,第一个参数传入的是基本类型数字或者字符串,绑定this时会把他们转换成对象

    function getThisType(){
        console.log('this指向',this,typeof this);
    }
    getThisType.call(1);// this指向 Number{1} object
    getThisType.apply('ccc');// this指向 String{'ccc'} object
    
    bind

    bind方法会创建一个新函数返回,当这个新函数被调用时,bind的第一个参数将作为它运行时的this,从bind的第二个参数开始的一系列参数会在这个新函数调用时传入的实参的前面传入,就是bind传入的第二个开始连接上新函数调用时传入的参数一起传递给新函数

    func.bind(this[, arg1[, arg2[, ...]]])
    
    var person = {
        name:'zhangsan',
        sayName:function(other){
            console.log(`${other} ${this.name}`);
        }
    }
    person.sayName('lisi')//lisi zhangsan
    
    var person2 = {
         name:'wangwu',
    }
    var sayName1 = person.sayName.bind(person2,'zhaoliu');
    sayName1();//zhaoliu wangwu
    
    var person3 = {
         name:'tom',
    }
    var sayName2 = person.sayName.bind(person3);
    sayName2('jam');//jam tom
    
    var person4 = {
         name:'小黑',
    }
    var sayName3 = person.sayName.bind(person4,'小白');
    sayName3('小红');//小白 小黑    这里传入了两个参数,bind里的传入在前,所以输出 小白 小黑
    
    call、apply和bind实现
    //call
    Function.prototype.call = function(context,...args){
        let context = context || window;
        context.fn = this;
        context.fn(...args);
        delete context.fn;
    }
    
    //apply
    Function.prototype.apply = function(context,argArr){
        let context = context || window;
        context.fn = this;
        context.fn(...argArr);
        delete context.fn;
    }
    
    //bind
    Function.prototype.bind = function(context,...args){
        let context = context || window;
         context.fn = this;
        return function(..._args){
             context.fn(...args,..._args);
             delete context.fn;
        }
    }
    
  4. new 绑定

    • 创建一个新对象
    • 将新对象的__proto__指向构造函数的prototype属性
    • 执行构造函数中的代码
    • 返回新对象
    function Person(name){
        this.name = name;
    }
    var person = new Person('ys');
    console.log(person.name);//ys
    

箭头函数

  • 箭头函数中没有arguments 普通函数可以通过arguments拿到所有参数, ⽽箭头函数不可以。如果要拿到所有箭头函数的参数, 我们可以直接⽤参数的解构:
let res = (...nums) => nums;
  • 箭头函数不能用作构造函数 箭头函数与普通函数不同,它没有prototype属性,无法将实例的__proto__属性指向它的prototype,如果是用new调用箭头函数会报错
  • 箭头函数没有自己的this 箭头函数中的 this 是在定义时的位置决定的,而不是像普通函数是在调用时才绑定
var name = 'haha';
var person = {
    name:'xixi',
    sayHi:sayHi
}
function sayHi(){
    console.log(this);//{name:xixi,sayHi:f}
    setTimeout(()=>{
        console.log(this.name);//'haha'
    })
}
person.sayHi();

闭包

概念

闭包是指那些能够访问自由变量的函数;
自由变量是指在函数中使用的,但既不是函数参数也不是函数局部变量的变量。

  1. 从理论角度:所有函数都是闭包。因为它们在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于访问自由变量,这时使用最外层的作用域。
  2. 从实际角度,以下函数才是闭包:
    • 即使创建它的上下文已经销毁,它仍然存在(函数作为外层函数返回值);
    • 在代码中引用了自由变量。
应用场景
  1. 柯里化函数 柯里化的目的:避免频繁调用具有相同参数的函数,同时可以复用。
//假设有一个求矩形面积的函数
function getArea(width,height){
    return width * height;
}
//如果宽度都是固定10,高度变化
const area1 = getArea(10,20);
const area2 = getArea(10,30);
const area3 = getArea(10,40);

//这里就重复参数10,可以使用闭包柯里化这个函数
function getArea(width){
    return function(height){
        return width * height;
    }
}
//之前的10宽度矩形可以如下计算
const getTenWidthArea = getArea(10);
const area1 = getTenWidthArea(20);
const area2 = getTenWidthArea(30);
const area3 = getTenWidthArea(40);
  1. 使⽤闭包实现私有⽅法/变量
    fucntion funOne(i){
        return function funTwo(){
            console.log(i);
        }
        return funTwo;
    }
    var f1 = funOne(10);
    var f2 = funOne(20);
    var f3 = funOne(30);
    f1();//10
    f2();//20
    f3();//30
    
  2. 匿名自执行函数
    var funOne = (function(){
        var num = 0;
        return function(){
            num++;
            return num;
        }
    })()
    console.log(funOne());//1
    console.log(funOne());//2
    console.log(funOne());//2
    
  3. 缓存一些结果
    function outer(){
        let arr = [];
        return function inner(i){
            arr.push(i)
            console.log(arr.join(','));
        }
    }
    const fn = outer();
    fn(1);//1
    fn(2);//1,2
    
总结
  • 创建私有变量
  • 延长变量生命周期 ⼀般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引⽤,即便创建时所在的执⾏上下⽂被销毁,但创建时所在词法环境依然存在,以达到延⻓变量的⽣命周期的⽬的

作用域

作⽤域是在运⾏时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作⽤域决定了代码区块中变量和其他资源的可⻅性。
作⽤域就是⼀个独⽴的地盘,让变量不会外泄、暴露出去。也就是说作⽤域最⼤的⽤处就是隔离变量,不同作⽤域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作⽤域,只有全局作⽤域和函数作⽤域。ES6 的到来,为我们提供了块级作⽤域,可通过新增命令letconst来体现。

  1. 全局作用域 在代码中任何地⽅都能访问到的对象拥有全局作⽤域。

    • 最外层函数 和在最外层函数外⾯定义的变量拥有全局作⽤域
    var ourterVariable = "outer" //外层变量
    function outFn(){            //外层函数
        var innerViriable = "inner"  //内层变量
        function innerFun() { //内层函数
            console.log(inVariable);
        }
        innerFun();
    }
    console.log(outVariable); //outer
    outFun(); //inner
    console.log(inVariable); //inVariable is not defined
    innerFun(); //innerFun is not defined
    
    • 所有未定义直接赋值的变量⾃动声明为拥有全局作⽤域
    function outFun2() {
    variable = "未定义直接赋值的变量";
    var inVariable2 = "内层变量2";
    }
    outFun2();
    console.log(variable); //未定义直接赋值的变量
    console.log(inVariable2); //inVariable2 is not defined
    
    • 所有window对象的属性拥有全局作⽤域
    • 弊端 变量全部定义在全局作⽤域中。这样就会污染全局命名空间, 容易引起命名冲突。
  2. 函数作⽤域 函数作⽤域,是指声明在函数内部的变量,和全局作⽤域相反,局部作⽤域⼀般只在固定的代码⽚段内可访问到,最常⻅的例如函数内部。

    function out(){
        var inViriable = "函数作用域变量";
        function inFn(){//函数内的函数
             alert(inViriable);
        }
        inFn();
    }
    alert(inViriable);//inViriable is not defined
    inFn();//inFn is not defined
    

    作⽤域是分层的,内层作⽤域可以访问外层作⽤域的变量,反之则不⾏

  3. 块级作⽤域 块级作⽤域可通过新增命令letconst声明,所声明的变量在指定块的作⽤域外⽆法被访问。 块级作⽤域在如下情况被创建:

    • 在⼀个函数内部
    • 在⼀个代码块(由⼀对花括号包裹)内部 let 声明的语法与 var 的语法⼀致。你基本上可以⽤ let 来代替 var 进⾏变量声明,但会将变量的作⽤域限制在当前代码块中。

块级作⽤域有以下⼏个特点:

  • 声明变量不会提升到代码块顶部
  • 禁止重复生命
  • 变量只在当前块有效
for(var i = 0;i < 10;i++){
    setTimeout(function(){
        console.log(i);
    })
}
// 输出 十个10
for(let i = 0;i < 10;i++){
    setTimeout(function(){
        console.log(i);
    })
}
// 输出 0 1 2 3 4 5 6 7 8 9

第⼀个变量i是⽤var声明的,在全局范围内有效,所以全局中只有⼀个变量i,每次循环时,setTimeOut定时器⾥指的是全局变量i,⽽循环⾥的⼗个setTimeOut是在循环结束后才执⾏,所以输出⼗个10。

第⼆个变量i是⽤let声明的,当前的i只在本轮循环中有效,每次循环的i其实都是⼀个新的变量,所以setTImeOut定时器的⾥⾯的i其实不是同⼀变量,所以输出0 1 2 3 4 5 6 7 8 9。

作用域链

类似于原型链,找⼀个变量的时候, 如果当前作⽤域找不到, 那就会逐级往上去查找, 直到找到全局作⽤域还是没找到,就真找不到了。

Tips: 最先在要到创建这个函数的那个域查找,这⾥强调的是“创建”,⽽不是“调⽤”

  • 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
  • 对于⼀个常量进⾏赋值,在 strict 模式下会报错,⾮ strict 模式下静默失败。
  • IIFE中的函数是函数表达式,⽽不是函数声明。
变量提升

js会把所有变量都集中提升到作⽤域顶部事先声明好,函数声明会提升到顶部变量的下方,但是它赋值的时机是依赖于代码的位置,那么js解析运⾏到那⼀⾏之后才会进⾏赋值,还没有运⾏到的就不会事先赋值。也就是变量会事先声明,但是变量不会事先赋值。 看下面代码输出:

function v() {
    console.log(a); 
    var a = 1;
    console.log(a);
    function a() {
    }
    console.log(a);
    console.log(b);
    var b = 2;
    console.log(b);
    function b() {
    }
    console.log(b);
}
v();
// 下面模拟一下执行过程
function v(){
    var a;
    var b;
    function a(){}
    function b(){}
    console.log(a);//fn a
    a = 1;
    console.log(a);//1
    console.log(a);//1

    console.log(b);//fn b
    b=2;
    console.log(b);//2
    console.log(b);//2
}
v();
// 所以依次输出 fa,1,1,fb,2,2