this指向以及作用域和闭包

2021-07-19  本文已影响0人  大南瓜鸭

一、关于this指向的几种场景

1、默认绑定(函数直接调用)

非严格模式下,默认绑定指向全局(node 中是 global

function fn() {
  console.log(this)
}
fn()
function fn() {
  'use strict'
  console.log(this) // undefined
}
fn()

把最外层 var a = 1 -> let a = 1

var a = 1
function fn() {
  var a = 2
  console.log(this.a) // 1
}
fn()
let a = 1
function fn() {
  var a = 2
  console.log(this.a) // undefined
}
fn()

指向调用者

var b = 1
function outer () {
  var b = 2
  function inner () { 
    console.log(this.b) // 1
  }
  inner()
}

outer()
const obj = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

obj.fn() // 1
const f = obj.fn
f() // undefined

2、隐式绑定(属性访问调用)

隐式绑定的 this 指的是调用堆栈的上一级.前面个)

function fn () {
    console.log(this.a)
  }
  
  const obj = {
    a: 1
  }
  
  obj.fn = fn
  obj.fn() // 1
function fn () {
  console.log(this.a)
}

const obj1 = {
  a: 1,
  fn
}

const obj2 = {
  a: 2,
  obj1
}

obj2.obj1.fn() // 1

隐式绑定失效指向哪里?

  1. 直接赋值,this指向改变
const obj1 = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

const fn1 = obj1.fn // 将引用给了 fn1,等同于写了 function fn1() { console.log(this.a) }
fn1() // 所以这里其实已经变成了默认绑定规则了,该函数 `fn1` 执行的环境就是全局环境
  1. setTimeout
setTimeout(obj1.fn, 1000) // 这里执行的环境同样是全局
  1. 函数作为参数传递
function run(fn) {
  fn()
}
run(obj1.fn) // 这里传进去的是一个引用
  1. 一般匿名函数也是会指向全局的
var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return function() { // 这是一个匿名函数
            console.log(this.name)
        };
    }
}
obj.getName()()

3、显式绑定(callbindapply

这种根本还是取决于第一个参数,但是第一个为 null 的时候还是绑到全局的

function fn () {
  console.log(this.a)
}
const obj = {
  a: 100
}
fn.call(obj) 

bind 只看第一个 bind(堆栈的上下文,上一个,写的顺序来看就是第一个)

function fn() {
  console.log(this) // 1
}

// 为啥可以绑定基本类型 ?
// boxing(装箱) -> (1 ----> Number(1))
fn.bind(1).bind(2)()

bind实现

//  Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
  var ArrayPrototypeSlice = Array.prototype.slice; // 为了 this
  Function.prototype.bind = function(otherThis) {
    // 调用者必须是函数,这里的 this 指向调用者:fn.bind(ctx, ...args) / fn
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var baseArgs= ArrayPrototypeSlice.call(arguments, 1), // 取余下的参数
        baseArgsLength = baseArgs.length,
        fToBind = this, // 调用者
        fNOP    = function() {}, // 寄生组合集成需要一个中间函数,避免两次构造
        fBound  = function() {
          // const newFn = fn.bind(ctx, 1); newFn(2) -> arguments: [1, 2]
          baseArgs.length = baseArgsLength; // reset to default base arguments
          baseArgs.push.apply(baseArgs, arguments); // 参数收集
          return fToBind.apply( // apply 显示绑定 this
            // 判断是不是 new 调用的情况,这里也说明了后边要讲的优先级问题      
            fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
          );
        };
        // 下边是为了实现原型继承
    if (this.prototype) { // 函数的原型指向其构造函数,构造函数的原型指向函数
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; // 就是让中间函数的构造函数指向调用者的构造
    }
    fBound.prototype = new fNOP(); // 继承中间函数,其实这里也继承了调用者了

    return fBound; // new fn()
  };
})();

4、new的指向

实现new

// new 关键字会进行如下的操作:

// 1. 创建一个空的简单JavaScript对象(即{});
// 2. 链接该对象(设置该对象的constructor)到另一个对象 ;
// 3. 将步骤1新创建的对象作为this的上下文 ;// 🔥
// 4. 如果该函数没有返回对象,则返回this。

// 我们来模拟实现一个 new
// new Fn(); 
// myNew(Fn, ...args);
import _ from 'lodash';

function myNew(fn, ...args) {
  // fn 必须是一个函数
  if (typeof fn !== 'function') throw new Error('fn must be a function.')
  // es6 new.target
  myNew.target = fn
  // 原型继承
  const temp = Object.create(fn.prototype) // 步骤 1. 2.
  // fn执行绑定 this 环境
  const res = fn.apply(temp, ...args) // 步骤 3.
  // 如果该函数没有返回对象,则返回this。
  return _.isObject(res) ? res : temp
}

如果函数 constructor 里没有返回对象的话,this 指向的是 new 之后得到的实例

function foo(a) {
  this.a = a
}

const f = new foo(2)
f.a // 2
function bar(a) {
  this.a = a
  return {
    a: 100
  }
}
const b = new bar(3)
b.a // 100

5、箭头函数

箭头函数本身是没有 this 的,继承的是外层的

function fn() {
  return {
    b: () => {
      console.log(this)
    }
  }
}

fn().b() // console what?
fn().b.bind(1)() // console what?
fn.bind(2)().b.bind(3)() // 2

6、优先级

「new 绑」 > 「显绑」 > 「隐绑」 > 「默认绑定」

// 隐式 vs 默认 -> 结论:隐式 > 默认
function fn() {
  console.log(this)
}

const obj = {
  fn
}

obj.fn() // what ?

// 显式 vs 隐式 -> 结论:显式 > 隐式
obj.fn.bind(5)() // what ?

// new vs 显式 -> 结论:new > 显式
function foo (a) {
    this.a = a
}

const obj1 = {}

var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // what ?

var baz = new bar(3)
console.log( obj1.a ) // what ?
console.log( baz.a ) // what ?

// 箭头函数没有 this,比较没有意义

7、实战题

function foo() {
  console.log( this.a ) // 2
}
var a = 2;
(function(){
  "use strict" // 迷惑大家的
  foo();
})();
var name="the window"

var object={
  name:"My Object", 
  getName: function(){ 
    return this.name
  } 
}
object.getName() // My Object
(object.getName)() // My Object

// 运算符导致丢失了this指向
(object.getName = object.getName)() // the window
(object.getName, object.getName)() // the window
var x = 3
var obj3 = {
  x: 1,
  getX: function() {
    var x = 5
    return function() {
      return this.x
    }(); // ⚠️
  }
}
console.log(obj3.getX()) // 3
function a(x){
  this.x = x
  return this
}
var x = a(5) // 替换为 let 再试试
var y = a(6) // 替换为 let 再试试 // 再换回 var,但是去掉 y 的情况,再试试

console.log(x.x) // undefind
console.log(y.x) // 6

// 等价于
window.x = 5;
window.x = window;

window.x = 6;
window.y = window;

console.log(x.x) // void 0 其实执行的是 Number(6).x
console.log(y.x) // 6

二、作用域和作用域链

作用域

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在ES5中,变量的作用域有全局作用域和局部作用域,局部作用域又称为函数作用域。ES6引入了块级作用域,明确允许在块级作用域中声明函数。

全局作用域:

局部作用域:

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),其实就是对执行上下文EC中的变量对象VO|AO有序访问的链表。能按顺序访问到VO|AO。

function a() {
        function b() {
            var b = 234;
        }
        var a = 123;
        b();
    }
var gloab = 100;
a();

当控制器转到一段可执行代码的时候就会进入到一个执行上下文。执行上下文是一个栈结构(先进后出), 栈底部永远是全局上下文,栈顶是当前活动的上下文。其余都是在等待的状态。
当浏览器首次载入你的脚本,它将默认进入全局执行上下文。
如果,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并穿件一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。浏览器将总会执行栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。

闭包

我们有时候需要得到函数内的局部变量,由于作用域的概念,在函数外部无法读取函数内的局部变量,但是在函数的内部,再定义一个函数,把变量作为返回值,我们就可以在函数外部读取它的内部变量了。
总之,闭包就是能够读取其他函数内部变量的函数,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

function f1(){

    var n=999;

    function f2(){
      alert(n);
    }
      
      // f2函数,就是闭包
    return f2;

  }

var result=f1();

result(); // 999

作用

  1. 可以读取函数内部的变量
  2. 让这些变量的值始终保持在内存中

注意事项

以上例子,f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(为解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存)回收。
由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以在退出函数之前,记得将不使用的局部变量全部删除,否则可能导致内存泄漏(不再用到的内存,没有及时释放,就叫做内存泄漏)。

应用场景

function fn(){
        var name='hello'
        setName=function(n){
            name = n;
        }
        getName=function(){
            return name;
        }
          
        //将setName,getName作为对象的属性返回
        return {
            setName:setName,
            getName:getName
        }
    }
    var fn1 = fn();//返回对象,属性setName和getName是两个函数
    console.log(fn1.getName());//getter
        fn1.setName('world');//setter修改闭包里面的name
    console.log(fn1.getName());//getter
var arr =['aa','bb','cc'];
function incre(arr){
    var i=0;
    return function(){
        //这个函数每次被执行都返回数组arr中 i下标对应的元素
         return arr[i++] || '数组值已经遍历完';
    }
}
var next = incre(arr);
console.log(next());//aa
console.log(next());//bb
console.log(next());//cc
console.log(next());//数组值已经遍历完
//每秒执行1次,分别输出1-10
for(var i=1;i<=10;i++){
    (function(j){
        //j来接收
        setTimeout(function(){
            console.log(j);
        },j*1000);
    })(i)//i作为实参传入
}
上一篇 下一篇

猜你喜欢

热点阅读