JS基础知识(三):作用域与闭包

224 阅读4分钟

作用域和自由变量

作用域

  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6新增)

自由变量

  • 一个变量在当前作用域没有定义,但被使用了
  • 向上级作用域,一层一层寻找,直到找到为止
  • 如果到全局作用域都没有找到,则报错xx is not defined

闭包

概念

闭包函数:声明在一个函数中的函数,叫做闭包函数。

闭包:内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。

特点

  • 让外部访问函数内部变量成为可能;
  • 局部变量会常驻在内存中;
  • 可以避免使用全局变量,防止全局变量污染;
  • 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)

闭包的创建

闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。
闭包会发生内存泄漏,每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址
但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。

闭包内存泄漏为: key = value,key 被删除了 value 常驻内存中; 局部变量闭包升级版(中间引用的变量) => 自由变量;

闭包应用的特殊情况,由两种表现:

  • 函数作为参数被传递
  • 函数作为返回值被返回

例:

// 函数作为返回值
function create() {
    let a = 100
    return function () {
        console.log(a)
    }
}

let fn = create()
let a = 200
fn() // 100
function print(fn) {
    let a = 200
    fn()
}

let a = 100
function fn () {
    console.log(a)
}
print(fn) // 100

由上两个例子可以得出:
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!!!

this

this调用的几种情况:

  • 作为普通函数调用
  • 使用callapplybind
  • 作为对象方法被调用
  • class方法中被调用
  • 箭头函数中调用

this取什么值在函数执行时确定,不是函数定义时确定

例一:

function fn1() {
  console.log(this);
}
fn(); // window

fn1.call({ x: 100 }); // this指向{x:100}

const fn2 = fn1.bind({ x: 200 }); // bind会返回一个新的函数
fn2(); // {x:200}

例二:

const zhangsan = {
  name: "张三",
  sayHi() {
    // this指向当前对象
    console.log(this);
  },
  wait() {
    setTimeout(function() {
      // this指向window
      // 因为此时的setTimeout这个方法作为普通函数进行执行
      // 并不是作为zhangsan这个对象的方法进行执行
      console.log(this);
    });
  }
};

例三:

const zhangsan = {
  name: "张三",
  sayHi() {
    // this指向当前对象
    console.log(this);
  },
  waitAgain() {
    setTimeout(() => {
      // this指向当前对象
      // 箭头函数的this永远取它上级作用域的this
      console.log(this);
    });
  }
};

例四:

class People {
  constructor(name) {
    this.name = name;
    this.age = 20;
  }
  sayHi() {
    console.log(this);
  }
}
const zhangsan = new People('张三')
zhangsan.sayHi() // zhangsan这个对象

手写call、apply和bind

call

手写call之前,需要分析一下需要几个步骤:
我们将调用方法的对象叫做A,将需要改变this的对象叫做B

  1. 判断是否传入参数,如果没有传入,那么默认为 window
  2. 给 context 添加一个临时属性,属性指向A
  3. 将 context 后面的参数取出来
  4. 将参数传入A
  5. 删除context临时添加的属性
Function.prototype.myCall = function(context) {
  // 判断是否传入参数,如果没有传入,那么默认为 window
  context = context || window;
  // 给 context 添加一个属性,属性指向当前对象
  context.fn = this;
  // 将 context 后面的参数取出来
  const args = [...arguments].slice(1);
  // 将参数传入当前对象
  const result = context.fn(args);
  delete context.fn;
  return result;
};

apply

applycall差不多,只不过apply只需要传入两个参数,call可以传入无数个参数

Function.prototype.myApply = function(context) {
  context = context || window;
  context.fn = this;
  // 需要判断是否存储第二个参数
  // 如果存在,就将第二个参数展开
  const result = arguments[1]
    ? context.fn(...arguments[1])
    : context.fn();
  delete context.fn;
  return result;
};

bind

bind与上方两个的区别是bind会返回一个函数

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}