作用域this及闭包

101 阅读4分钟

课程目标

  • this
  • 闭包
  • 作用域

知识点

  • 作用域 + 上下文
  • 闭包常见场景

作用域链

先看一段简单的代码:

let a = 'global';
console.info(a);

function course() {
    let d = 'd';
    console.info(d);

    session();
    function session() {
        let b = 'this';
        console.info(b);

        teacher();
        function teacher() {
            let c = 'yy';
            console.info(c);
            
            console.info(d);
        }
    }
}
course();

是能正常输出的:如下图

image.png

  1. 对于作用域链我们直接通过创建态来定位作用域链
  2. 手动取消全局,使用块级作用域

this 上下文context

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

函数直接调用中

  • this指向的是window => 函数表达式、匿名函数、嵌套函数
function foo() {
    console.info('函数内部this', this);
}
foo();

image.png

隐式绑定

  • this的指向是调用堆栈的上一级 => 对象、数组等引用关系逻辑
function fn() {
    console.info('隐式绑定', this.a);
}
const obj = {
    a: 1
}
obj.fn = fn;
obj.fn();

输出结果:

image.png

  • 其实执行到obj.fn = fn这句代码就相当于是:
const obj = {
    a: 1,
    fn
}
  • 面试题:this的输出结果是什么?
const foo = {
    bar: 10,
    fn: function() {
        console.info(this.bar);
        console.info(this);
    }
}
let fn1 = foo.fn;
fn1();

输出结果:

image.png 从结果能看到this指向window是因为fn1只是把foo.fn方法取出并没有执行。fn1()后面单独执行。

  • 追问:如何改变指向
const a1 = {
    text: 'a1',
    fn: function() {
        //传统。直接使用上下文
        return this.text
    }
}
const a2 = {
    text: 'a2',
    fn: function() {
        //间接使用上下文
        return a1.fn()
    }
}
const a3 = {
    text: 'a3',
    fn: function() {
        //直接内部构造 - 公共
        let fun = a1.fn;
        return fun()
    }
}
console.info('a1fn:',a1.fn());
console.info('a2fn:',a2.fn());
console.info('a3fn:',a3.fn());

输出结果:

image.png

  1. 在执行函数时,函数被上一级调用,上下文指向上一级
  2. or 直接变成公共函数,指向window
改变this

this指向最后调用他的对象.

const a1 = {
    text: 'a1',
    fn: function() {
        //传统。直接使用上下文
        return this.text
    }
}
const a2 = {
    text: 'a2',
    fn: a1.fn
}
console.info('a2fn:',a2.fn());

image.png

显示绑定 - bind/call/apply

使用方法:

function foo() {
    console.info('函数内部this',this);
}
foo();
foo.call({a: 1}):
foo.apply({a: 1});
const bindFoo = foo.bind({a: 1});
bindFoo();
call、apply、bind的区别
  1. call、apply传参不同,
    • call 依次传入
    • apply 数组传入
  2. bind 直接返回不同
new - this指向的是new之后得到的实例
class Course {
    constructor(name) {
        this.name = name;
        console.info('构造函数中的this',this);
    }
    test() {
        console.info('类方法中的this',this);
    }
}
const course = new Course('this');
course.test();

image.png

类中异步方法,this有区别吗?
class Course {
    constructor(name) {
        this.name = name;
        console.info('构造函数中的this',this);
    }
    test() {
        console.info('类方法中的this',this);
    }
    asyncTest() {
         console.info('异步方法外的this',this);
         setTimeout(function() {
             console.info('异步方法内的this',this);
         },1000);
    }
}
const course = new Course('this');
course.test();
course.asyncTest();

image.png

  1. 执行setimeout时,匿名方法执行时,效果和全局执行函数效果相同。
  2. 再追问:如果把setimeout的this改成指向实例怎么办?改成箭头函数
class Course {
    asyncTest() {
         console.info('异步方法外的this',this);
         setTimeout(()=> {
             console.info('异步方法内的this',this);
         },1000);
    }
}
const course = new Course('this');
course.asyncTest();

image.png

bind的原理/手写bind
  1. 说明原理,写下注释
  2. 根据注释,补齐代码
function sum(a,b,c) {
    return a + b + c;
}
//1. bind挂载在哪里?- Function.prototype
Function.prototype.newBind = function() {
    //2. bind是什么?- (1)返回一个函数,(2)返回原函数执行结果,(3)传参不变
    const _this = this;
    const args = Array.prototype.slice.call(arguments);
    //arguments的特点:第一项是新的this,第二项是最后一项函数传参
    const newThis = args.shift();
    return function() {
        return _this.apply(newThis, args);
    }
}
实现apply
Function.prototype.newApply = function(context) {
    //边缘测试
    //函数测试
    if (typeof this !== 'function') {
        throw new TypeError('Error');
    }
    //参数测试
    context = context || window;
    //挂载执行函数
    context.fn = this;
    //执行函数
    let result = arguments[1]
        ? context.fn(...arguments[1])
        : context.fn();
    //销毁临时挂载
    delete context.fn;
    return result;
}

如何突破作用域束缚 - 闭包

闭包

闭包:一个函数和他周围状态的引用捆绑在一起的组合

函数作为返回值的场景

function mail() {
    let content = 'mail';
    return function() {
        console.info(content);
    }
}
const envelop = mail();
envelop();

通过上面的例子可以看出:函数外部获取到了函数作用域内的变量值

函数作为参数的时候
//单一职责
let content;
//通用存储
function envelop(fn) {
    content = 1;
    fn();
}
//业务逻辑
function mail() {
    console.info(content);
}

envelop(mail);
函数嵌套
let counter = 0;

function outerfn() {
    function innerfn() {
        counter++;
        console.info(counter);
    }
    return innerfn;
}
outerfn()();

image.png

事件处理(异步执行)的闭包
let lis = document.getElementsByTagName('li');

for(let i = 0; i < lis.length; i++){
    (function(i) {
        lis[i].onclick = function(){
            console.info(i);
        }
    })(i);
}
立即执行嵌套
(function immediateA(a) {
    return (function immediateB(b) {
        console.info(a);//0
    })(1);
})(0);

image.png

当立即执行遇上块级作用域:
let count = 0;

(function immediate() {
    if(count === 0) {
        let count = 1;
        console.info(count);
    }
    console.info(count);
})();

image.png

拆分执行
function createI() {
    let count = 0;
    function increment() {
        count++;
    }
    let message = `count is ${count}`;
    
    function info() {
        console.info(message);
    }
    return [increment, info]
}
const [increment, info] = createI();
increment();
increment();
increment();
info();

image.png

  • 为什么没有新增成功?
  • 因为改变的是内部变量count ,打印的是内部变量message,实际传递出来的是函数。
实现私有变量
function create() {
    return {
        items: [],
        push(item) {
            this.items.push(item);
        }
    }
}
const stack = {
    items: [],
    push: function() {}
}

function createS() {
    //私有变量
    const items = [];
    return {
        push(item) {
            items.push(item);
        }
    }
}