javascript基础知识:作用域&this指向

77 阅读4分钟

作用域与作用域链

作用域链

  • 保证对执行环境有权访问的所有变量和函数的有序访问。
  • 案例分析
/**
 * 在函数内部可以一次按顺序访问在test3中可以访问test1、test2中所有的变量
 * 无法在test1中访问到,test2、test3中的变量
 * 沿着作用域链一级一级的搜索,然后逐级地向上找,直至找到该变量为止,如果找不到,会报错
 */
let a = "a";
function test1() {
  let b = "b";
  test2()
  function test2() {
    let b = "b1";
    // 打印:a b1 b会逐级往下找到最近的
    console.log(a, b)
    function test3() {
      let d = "d";
      console.log(a, b, d)
    }
    test3()
  }
}
test1()
  • 观察上述案例我们会发现,无论是在函数前面定义,使用函数,还是在函数定义后面使用函数,都是可以正常使用过的,为什么呢?
    • 其实是方便开发者,不需要关注何时定义,可以直接调用。
    • 也为目前打包工具tree-shaking埋下了伏笔。如果有未调用的,就可以通过tree-shaking将其去除。

作用域

  • 块级作用域
    • 使用{}扩起来的区域叫做块级作用域,if语句和for语句里面的{}。
    • 所有用let和const声明的变量符合块作用域。
    • 在块内使用let声明的变量,只会在当前的块内有效。
  • 案例分析
// 通过 var 变量定义会提前声明定义。并不会产生块级作用域效果
if (true) {
  var b = 2;
}
// 打印 2
console.log(b)

// 通过 let、const 变量定义只在当前块内生效
if (true) {
  let c = 2;
  console.log(c)
}
// 报错:Uncaught ReferenceError: c is not defined
console.log(c)
  • 函数作用域
    • 局部作用域,只和函数有关系,和其他外部无关。他也为现在的es module打下基础
    • 在函数中声明var变量也是局部的,不会像块级一样辐射到外面。
  • 案例分析
// 通过 var、let、const 变量定义都只在函数里面起作用
function test() {
  var b = 2;
  let c = 3;
  const d = 4;
}
// 报错:Uncaught ReferenceError: b、c、d is not defined
console.log(b, c, d)

this指向

  • this指向是动态执行的,简单来说就是,谁调用this指向谁
  • 案例分析
const obj = {
  text: 1,
  fn: function () {
    return this.text
  }
}
/**
 * 我们看到目前是obj调用的方法,那他的指向显然是obj所以打印为1
 * 打印为:1。
 */
console.log(obj.fn())
const fn = obj.fn
/**
 * 我们看到目前看到虽然是赋值,但是调用时他是直接调用的,赋值并不会改变指向。
 * this也是动态,所以一定要等到调用功能的时候再看。
 * 直接调用,我们会发现this指向的是window,里面没有text所以打印undefined
 * 打印为:undefined。
 */
console.log(fn());
  • 案例分析
const obj1 = {
  text: 2,
  fn: function () {
    return this.text
  }
}
const obj = {
  text: 1,
  fn: function () {
    return obj1.fn()
  }
}
/**
 * 中间过程其实可以忽略,我们直接看谁调用,发现是obj1调用fn,他的指向很显然就是obj1。
 * 打印为:2。
 */
console.log(obj.fn())
const obj1 = {
  text: 2,
  fn: function () {
    return this.text
  }
}
const obj = {
  text: 1,
  fn: function () {
    const fn = obj1.fn
    return fn()
  }
}
/**
 * 原理同上,你看我们虽然赋值了,但是调用的并不是obj1,而是直接调用,所以他的this指向的是window。
 * 打印为:undifined。
 */
console.log(obj.fn())

综合上面三个案例来看,其实无论前置如何,最后都要看是谁来调用。才能决定this是谁。

  • 如何更改this指向
    • call、apply、bind
  • 案例分析
const obj1 = {
  text: 2,
}
const obj = {
  text: 1,
  fn: function (a) {
    return this.text + a
  }
}
// 打印: 5
console.log(obj.fn.call(obj1, 3), "call")
console.log(obj.fn.apply(obj1, [3]), "apply")
console.log(obj.fn.bind(obj1)(3), "bind")
console.log(obj.fn.bind(obj1, 3)(), "bind")
  • 三种方式其实类似,但是也是有区别的
    • apply传入函数的入参是数组的形式,call则是和函数相同
    • bind是生成一个新的函数,参数既可以传bind,也可以传入新生成的函数中
  • 手写bind
    • 注意事项:new 的情况要考虑
/**
 * bind绑定到Function.prototype中
 * 考虑是否是new的情况
 * 实现myApply
 */
Function.prototype.myBind = function (context, ...bindArgs) {
  const _this = this;
  const fBound = function (...args) {
    // 如果时候new 返回this
    thisArg = this instanceof fBound ? this : context;
    return _this.myApply(thisArg, [...bindArgs, ...args])
  }
  fBound.prototype = Object.create(this.prototype);
  return fBound
}
/** 
 * 找到执行函数 this
 * 传入函数,入参
 * 删除挂载函数
 */
Function.prototype.myApply = function (context, args = []) {
  // 边界判断
  if (typeof this !== 'function') {
    throw new TypeError('Error is not a function')
  }
  const symbol = Symbol('fn');
  context = context || window;
  context[symbol] = this;
  const result = context[symbol](...args);
  delete context[symbol];
  return result
}

function testFc(params) {
  console.log(this, "xx", params)
}
testFc.myBind({ a: 2 }, 3)()