四 作用域和闭包

110 阅读6分钟

// 点击每个都打印10
let i, a;
for(i=0; i<10; i++){
  a = document.createElement('a');
  a.innerHTML = i + '<br>';
  a.addEventListener('click', function(e){
    e.preventDefault();
    alert(i);
  })
  document.body.appendChild(a)
}

// 这样就点击每个都打印对应的i了
let a;
for(let i=0; i<10; i++){
  a = document.createElement('a');
  a.innerHTML = i + '<br>';
  a.addEventListener('click', function(e){
    e.preventDefault();
    alert(i);
  })
  document.body.appendChild(a)
} 

知识点

  • 作用域和自由变量
  • 闭包
  • this

作用域

image.png

作用域:变量可以合法使用的范围

分为:

  • 全局作用域:代码中直接写一个变量,变量不受函数的约束,在全局中都可以使用,比如window、document
  • 函数作用域:函数中定义的对象,只能在当前函数内使用
  • 块级作用域(ES6新增):块 例如if for while大括号,大括号中就是块
if(true){
  let x = 100;
}
console.log(x) // 会报错,let const定义的变量,都遵循块级作用域的规则

自由变量

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

闭包

闭包实际上是作用域应用的特殊情况,有两种表现:

跟上方四个红方的函数不一样(函数定义在什么地方,就会在什么地方调用)

  • 函数作为参数被传递 (函数在一个地方定义好后,传递到另一个地方去执行)
  • 函数作用返回值被返回(函数在一个地方定义好后,被返回到另一个地方去执行)
  • 总之,函数定义的地方和执行的地方不一致
// 函数作为返回值
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取什么值,是在函数执行的时候决定的,不是在函数定义的时候决定的

  • 作为普通函数: this指向window
function fn1(){
  console.log(this)
}
fn1() // window
  • 使用call,apply,bind: this指向第一个参数
fn1.call({x: 100}) // {x: 100}

const fn2 = fn1.bind({x: 200})
fn2() // {x: 200}
// bind是返回一个新的函数,执行下这个新的函数就好;call和apply是直接被执行的
  • 作为对象方法被调用:this指向对象
const zhangsan = {
  name: '张三',
  sayHi(){
    // this 即当前对象
    console.log(this)
  },
  wait(){
    console.log(this) // zhangsan
    setTimeout(function(){
      // this === window
      // 这句被执行,是setTimeout本向触发的执行,作为普通函数被执行;而不是作为zhangsan的方法被执行
      console.log(this) 
    });
  }
}
  • 在class方法中调用:在class中this值的就是class创建出来的实例对象
class People {
  constructor(name){
    this.name = name;
    this.age = 20;
  }
  sayHi(){
    console.log(this);
  }
}
const zhangsan = new People('张三');
zhangsan.sayHi() // zhangsan对象
  • 箭头函数中调用:this永远指向上级作用域的this
const zhangsan = {
  name: '张三',
  sayHi(){
    // this 即当前对象
    console.log(this)
  },
  waitAgain(){
    console.log(this) // zhangsan
    setTimeout(()=>{
      // this即当前对象
      console.log(this)
    })
  }
}

面试题二:手写bind

Function.prototype.bind1 = function(){
  // 1 将参数拆解为数组
  let args = Array.prototype.slice.call(arguments); // arguments是个列表,其实是个类数组,通过这行代码将arguments转为数组

  // 2 获取 this (数组第一项)
  const t = args.shift(); // shift挖出第一项,影响原数组

  // 3 fn1.bind(...) 中的 fn1
  const self = this;

  // 4 返回一个函数
  return function(){
    self.apply(t, args)
  }
}

function fn1(a, b){
  console.log('this', this);
  console.log(a, b);
  return 'this is fn1';
}

const fn2 = fn1.bind1({x: 100}, 10, 20)
const res = fn2();
console.log(res)

面试题三:实际开发中闭包的应用

隐藏数据(比如做一个简单的catch工具)

  • 我们通过 createCache中 return的函数,闭包的一种形式,jQuery中很常见
  • 执行c.set,c.get时,在createCache的作用域里的,能找到 data, 对data做修改
function createCache(){
  const data = {}; // 闭包中的数据,被隐藏,不被外界访问
  return {
    set: function(key, val){
      data[key] = val
    },
    get: function(key){
      return data(key)
    }
  }
}

const c = createCache();
c.set('a', 100);
console.log(c.get('a'));

// 不通过set get直接改 data的值,没法改,
// 因为 data定义在 createCache作用域里面的,不会被外界访问到; 
// 也就是说 data的合法作用域, 只是createCache里,不会被外界访问到
data.a = 200 // 会报错, data 作用域没有定义, 所以不会找到 createCache里

面试题四:点击每个a,打印结果是什么

每次点击都是10

解释:for循环在很短的时间内就循环完了,i的值就变成了10,因为i的作用域是全局,这个时候点击事件还没触发,所以再点击时弹出来的就是10

let a, i;
for(i=0; i<10; i++){
  a = document.createElement('a');
  a.innerHTML = i+ '<br/>';
  a.addEventListener('click', function(e){
    e.preventDefault();
    console.log(i)
  })
  document.body.appendChild(a)
}

每次点击都是相应的i值

解释:每次for循环时,形成了块级作用域,click在块级作用域中寻找i

  • 1 let i定义在for中,是定义的for里面的块作用域
  • let i在for中定义时,每次for循环执行的时候,都会形成一个新的块,块是针对 每一个块的**,产生一个块级作用域**,i就会不一样
  • 也就是说从0~9,i寻找的时候,就会从块级作用域去找, 就会打印对应的i
  • i 是全局作用域还是 块级作用域,是不一样的
  • let i在上方定义时,全局作用域针对所有的块的

重点:不会立马执行的函数,要注意,可能会对里面的自由变量,可能对外面的作用域里的数据改变所误导

let a;
for(let i=0; i<10; i++){
  a = document.createElement('a');
  a.innerHTML = i+ '<br/>';
  a.addEventListener('click', function(e){
    e.preventDefault();
    console.log(i)
  })
  document.body.appendChild(a)
}

小结

  • 作用域和自由变量
  • 作用域(几个红框)和自由变量(当前作用域中没有定义的变量,但是使用了,向上级作用域中查找)
  • 闭包:两种常见方式(返回函数、函数做为参数传递) && 自由变量查找规则(函数定义的时候查找)
  • this:函数执行的时候查找(this只有在执行的时候值才确定)

原型中的this

xiaoluo.proto.sayHi() // 姓名 undefined,学号 undefined

//类
//class + 名称 模板
class Student { 
  //当前构建的实例
  constructor(name,number){
    this.name = name;
    this.number = number;
  }
  sayHi( ) {//方法
    console.log(
        `姓名 ${this.name},学号 ${this.number}`//反引号
    )
  }
}

//通过类 new 对象/实例
const xiaoluo = new Student('夏洛',100 );

xiaoluo.sayHi() // 姓名 夏洛,学号 100
xiaoluo.__proto__.sayHi()  // 姓名 undefined,学号 undefined
  • obj.proto.sayHi()的时候 this指向了obj.proto,而这个对象里没有name和number,故为undefined。
  • 实际上obj.sayHi()执行原理类型于obj.prototype.sayHi.call(obj),用到了call,把this指向了obj,obj里有name和number,所以能打印出来。