this指向、作用域、闭包

1、this指向

参考资料:MDN中this解析
1、对于直接调用的函数来说,不管函数被放在了什么地方,this都是window
2、对于被别人调用的函数来说,被谁点出来的,this就是谁
3、call、apply时,this时第一个参数。bind要优于call/apply哦,call参数多,apply参数少
4、在构造函数中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例
5、箭头函数没有自己的this,需要看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window

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

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

// 1、非严格模式
function myfunc() {
  console.log(this);
}
myfunc(); // window

// 2、严格模式
function fn() {
  "use strict"; // 严格模式下,禁止this关键字指向全局对象
  console.log(this);
}
fn(); // undefined

// 面试题1
var a = 1;
function fn() {
  var a = 2;
  console.log(this.a); // console what ?
}
fn(); // 1

// 面试题2
let a = 1; // let 定义自己的作用域,不挂载至window
function fn() {
  var a = 2;
  console.log(this.a);
}
fn(); // undefined

// 面试题3
var b = 1;
function outer() {
  var b = 2;
  function inner() {
    console.log(this.b);
  }
  inner();
}
outer(); // 1

// 面试题4
const obj = {
  a: 1,
  fn: function() {
    console.log(this.a);
  }
};

obj.fn(); // 1
const f = obj.fn;
fn(); // 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、函数赋值
const obj1 = {
  a: 1,
  fn: function() {
    console.log(this.a);
  }
};
const fn1 = obj1.fn; // 将引用给了fn1,等同于写了 function fn1() { console.log(this.a); }
fn1(); // undefined
// 2、setTimeout
setTimeout(obj1.fn, 1000); // undefined
// 3、函数作为参数传递
function run(fn) {
  fn();
}
run(obj1.fn); // undefined, 传进去的是一个引用
// 4、一般匿名函数也是会指向全局的
var name = "The Window";
var obj = {
  name: "My obj",
  getName: function() {
    return function() {
      // 这是一个匿名函数
      console.log("this.name", this.name); // this.name The Window
    };
  }
};
obj.getName()();
// IIFE
(function() {
  var a = 1;
  console.log(this.a, this);
})(); // undefined

3、显示绑定(apply\call\bind)

  • apply
    • func.apply(thisArg, [argsArray]),thisArg为undefined或null时指向全局
    • 返回调用有指定this值和参数的函数的结果
  • call
    • function.call(this.Arg, arg1, arg2, ...)
    • 返回使用调用者提供的 this 值和参数调用该函数的返回值,若该方法没有返回值,则返回 undefined
  • bind
    • function.bind(thisArg[, arg1[, arg2[, ...]]])
      • thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。如果使用 new 运算符构造绑定函数,则忽略该值。当使用 bindsetTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object 。如果 bind 函数的参数列表为空,或者 thisArgnullundefined ,执行作用域的 this 将被视为新函数的 thisArg
      • arg1, arg2, ...:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
    • 返回:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
    • MDN的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()
    };
  })();
function fn() {
  console.log(this.a);
}
const obj = {
  a: 100
};
fn.call(obj); // 100

function fn() {
  console.log(this);
}

/**
为啥可以绑定基本类型?
boxing(装箱) -> (1 ----> Number(1))
bind 只看第一个bind(堆栈的上下文,上一个,写的顺序来看就是第一个)
 */
fn.bind(1).bind(2)(); // 1

面试题资料:手写bind\apply\call

4、new创建对象

如果函数 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、箭头函数

编译期间确定的上下文,不会被改变,哪怕你 new ,指向的就是上一层的上下文,箭头函数没有自己的 this,需要看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window

var a = {
  myfunc: function() {
    setTimeout(function() {
      console.log(this); // this是window
    }, 0);
  }
};
a.myfunc(); // window

var a = {
  myfunc: function() {
    var that = this;
    setTimeout(function() {
      console.log(that); // this是a
    }, 0);
  }
};
a.myfunc(); // a对象:{myfunc: f}

// 箭头函数
var a = {
  myfunc: function() {
    setTimeout(() => {
      console.log(this); // this是a
    }, 0);
  }
};
a.myfunc(); // a对象:{myfunc: f}

function fn() {
  return {
    b: () => {
      console.log(this);
    }
  };
}
fn().b(); // window
fn().b.bind(1)(); // window
fn.bind(2)().b.bind(3)(); // Number(2)

6、优先级

[new 绑] > [显 绑] > [隐 绑] > [默认 绑定]

function fn() {
  console.log(this);
}
const obj = {
  fn
};
obj.fn(); // obj对象:{fn: f}

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

// new vs 显示 -> 结论:new > 显示
function foo(a) {
  this.a = a;
}
const obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2

// new
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

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

2、作用域

1、数据存储

  • 栈(FIFO先进先出)放置静态内存,编译器分配:全局变量
  • 堆(FILO先进后出)放置动态内存,执行期分配:函数执行上下文

2、执行上下文

1、一文说透执行上下文(opens new window)
2、js深入之作用链
当函数执行时,会创建一个执行上下文的环境,分为创建和执行两个阶段:

  • 创建阶段:函数被调用未执行任何代码时,创建一个拥有三个属性的对象
executionContext = {
  scopeChain: {}, // 创建作用域链(scope chain)
  variableObject: {}, // 初始化变量、函数、形参
  this: {} // 指定 this
}; // 初始化 VO -> 建立作用域链 -> 确定 This 上下文
  • 执行阶段:分配变量(提升)、函数的引用,赋值;执行代码。
function demo(num) {
  var name = "xiaowa";
  var getData = function getData() {};
  function c() {}
}
demo(100);

// 创建阶段大致这样,在这个阶段就出现了【变量提升(Hositing)】
executionContext = {
  scopeChain: { ... },
  variableObject: {
    arguments: {
      // 创建了参数对象
      0: 100,
      length: 1
    },
    num: 100, // 创建形参名称,赋值/或创建引用拷贝
    c: pointer to function c(), // 有内部函数声明的话,创建引用指向函数体
    name: undefined, // 有内部声明变量a,初始化为undefined
    getData: undefined // 有内部声明变量b,初始化为undefined
  }, 
  this: { ... } 
}; 

// 代码执行阶段,在这个阶段主要是赋值并执行代码
executionContext = {
  scopeChain: { ... },
  variableObject: {
    arguments: {
      0: 100,
      length: 1
    },
    num: 100,
    c: pointer to function c(),
    name: "xiaowa", // 分配变量,赋值
    getData: pointer to function getData() // 分配函数的引用,赋值
  },
  this: { ... } 
};

3、执行上下文栈

  • 浏览器中的js解释器是单线程的,相对于浏览器中同一时间只能做一个事情
  • 代码中只有一个全局执行上下文和无数个函数执行上下文,这些组成了执行上下文栈(Execution Stack)
  • 一个函数的执行上下文,在函数执行完毕后,会被移出执行上下文栈。
function c() {
  console.log("ok");
}

function a() {
  function b() {
    c();
  }
  b();
}

a();

4、作用域与作用域链

  • 作用域:全局作用域、函数作用域、ES6块级作用域。作用域的最大用途就是隔离变量或函数,并控制生命周期。作用域在函数执行上下文创建时定义好的,不是函数执行时定义的
  • 作用域链:当一个块或函数嵌套在另一个块或函数中时,发生了作用域嵌套。在当前函数中如果无法找到某个变量,就会往上一级嵌套的作用域中去寻找,直到找到该变量或抵达全局作用域,这样的链式关系称为作用域链(Scope Chain)
    • 代码执行流没进入一个新上下文(全局、函数、块级作用域),就会创建一个作用域链,用于搜索变量和函数
    • 函数或块的局部上下文可以访问自己作用域内的变量,也可以访问任何包含上下文及全局上下文的变量。
    • 全局上下文只能访问全局作用域的变量和函数,无法访问局部上下文的任何数据
function a() {
  return function b() {
    var myname = "b";
    console.log(myname); // b
  };
}

function c() {
  var myname = "c";
  b();
}

var b = a();
c(); // 输出b

// 去掉函数b中的myname声明后
function a() {
  return function b() {
    // var myname = 'b'
    console.log(myname); // Uncaught ReferenceError: myname is not defined
  };
}

function c() {
  var myname = "c";
  b();
}

var b = a();
c();

5、作用域链增强

某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。

  • try/catch语句中catch块:会创建一个新的变量对象,包含要抛出的错误对象的申明
  • with语句:向作用域链前端添加制定的对象
// 范例1
var a = 15,
  b = 15;
with ({ a: 10 }) {
  var a = 30,
    b = 30;
  console.log(a); // 30
  console.log(b); // 30
}

console.log(a); // 15, 原值
console.log(b); // 30, b被更新

// 范例2
var a = 15,
  b = 15;
with ({ a: 10 }) {
  b = 30;
  console.log(a); // 10, 指定对象a的值
  console.log(b); // 30
}

console.log(a); // 15, 原值
console.log(b); // 30, b被更新

3、闭包

高级程序设计:闭包指有权访问另一个函数作用域中的变量的函数,可以理解为那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
使用场景:

  • 封装私有变量(AMD的框架等都使用)
// 普通定义类
function Person() {
  this.attackVolume = 100;
}
Person.prototype = {
  attack() {
    this.attackVolume += 1;
  }
};
var person = new Person();
console.log(person.attackVolume);

// 工厂方法
function Person() {
  return {
    attackVolume: 100,
    attack() {
      this.attackVolume += 1;
    }
  };
}
var person = new Person();
console.log(person.attackVolume, person);
  • 存储变量
function getList() {
  let localData = null;
  return {
    getData() {
      if (localData) {
        return Promise.resolve(localData);
      }
      return fetch("http://www.baidu.com").then(res => {
        localData = res.json();
      });
    }
  };
}

const listDataManager = getList();
window.onscroll = () => {
  Text.innerHTML = listDataManager.getData(); // 可能获取缓存数据
};

4、面试题

1、this指向

function foo() {
  console.log(this.a);
}
var a = 2;
(function() {
  "use strict";
  foo();
})(); //2,类似函数直接执行,'use strict'为迷惑,放在foo函数中为undefined

function foo() {
  "use strict";
  console.log(this.a);
}
var a = 2;
(function() {
  foo();
})(); //Uncaught TypeError: Cannot read properties of undefined (reading 'a')

var name = "the window";
var a = {
  name: "My Object",
  getName: function() {
    return this.name;
  }
};
a.getName(); // My Object
// (a.getName)(); // My Object
(a.getName = a.getName)(); // the window,函数赋值后,this指向全局
(a.getName, a.getName)(); // the window,函数操作后,this指向全局

var x = 3;
var obj3 = {
  x: 1,
  getX: function() {
    var x = 5;
    return (function() {
      return this.x;
    })();
  }
};
console.log(obj3.getX()); // 3,立即执行函数this指向全局

function a(x) {
  this.x = x;
  return this;
}
var x = a(5);
var y = a(6);
console.log(x.x); // undefined
console.log(y.x); // 6

/** 解析
window.x = 5;
window.x = window;
window.x = 6;
window.y = window;
console.log(x.x); // void 0 其实执行的时 Number(5).x
console.log(y.x); // 6
 */
// 隐式绑定
function show() {
  console.log("this:", this); // this指向obj
}
var obj = {
  show: show
};
obj.show(); // this: { show: [Function: show] }

// 隐式绑定失败场景
function show() {
  console.log("this:", this); // this指向obj
}
var obj = {
  show: function() {
    show(); // 类似于函数直接调用
  }
};
obj.show(); // this: window

var obj = {
  show: function() {
    console.log("this:", this);
  }
};
(0, obj.show)(); // this: window, 逗号表达式返回函数,函数再执行,let f = (0, obj.show), f()

// 隐式绑定失败场景
var obj = {
  sub: {
    show: function() {
      console.log("this:", this);
    }
  }
};
obj.sub.show(); // this: {show: f},sub对象,sub.出来的

// 优先级与显示绑定
var obj = {
  show: function() {
    console.log("this:", this);
  }
};
var newobj = new obj.show(); // this为newobj,输出this:show {},类似new function(){console.log("this:", this);}

var obj = {
  show: function() {
    console.log("this:", this);
  }
};
var newobj = new (obj.show.bind(obj))(); // new优先级强,this为newobj,输出this:show{}

// 事件触发
var obj = {
  show: function() {
    console.log("this:", this);
  }
};
var elem = document.getElementById("book-search-results");
elem.addEventListener("click", obj.show); // this为elem对象
elem.addEventListener("click", obj.show.bind(obj)); // this为obj,显示绑定
elem.addEventListener("click", function() {
  obj.show();
}); // this为obj,执行时为obj

2、作用域链

var person = 1;
function showPerson() {
  var person = 2;
  console.log(person);
}
showPerson(); // 2

var person = 1;
function showPerson() {
  console.log(person);
  var person = 2;
}
showPerson(); // undefined

var person = 1;
function showPerson() {
  console.log(person);
  var person = 2;
  function person() {} // 函数声明也可提前
}
showPerson(); // ƒ person() {}

var person = 1;
function showPerson() {
  console.log(person);
  function person() {}
  var person = 2;
}
showPerson(); // ƒ person() {}, 函数提升在变量提升之前

for (var i = 0; i < 10; i++) {
  console.log(i);
} // 0~9

for (var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i);
  }, 0);
} // 10~10, 注意for循环结束后,i还会++

for (var i = 0; i < 10; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, 0);
  })(i);
} // 0~9

for (let i = 0; i < 10; i++) {
  console.log(i);
} // 0~9

for (let i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i);
  }, 0);
} // 0~9

3、面向对象

function Person() {
  this.name = 1;
  return {};
}
var person = new Person();
console.log("name:", person.name); // name: undefined

function Person() {
  this.name = 1;
}
Person.prototype = {
  show: function() {
    console.log("name is:", this.name);
  }
};
var person = new Person();
person.show(); // name is: 1, 可执行原型链的方法,作用域链从当前开始

function Person() {
  this.name = 1;
}
Person.prototype = {
  name: 2,
  show: function() {
    console.log("name is:", this.name);
  }
};
var person = new Person();
Person.prototype.show = function() {
  // 覆盖原型链上的show方法
  console.log("new show");
};
person.show(); // new show

function Person() {
  this.name = 1;
}
Person.prototype = {
  name: 2,
  show: function() {
    console.log("name is:", this.name);
  }
};
var person = new Person();
var person2 = new Person();
person.show = function() {
  // 这里是person
  console.log("new show");
};
person2.show(); // name is: 1
person.show(); // new show

4、综合题

function Person() {
  this.name = 1;
}
Person.prototype = {
  name: 2,
  show: function() {
    console.log("name is:", this.name);
  }
};
Person.prototype.show(); // name is: 2
new Person().show(); // name is: 1

posted @ 2023-02-21 15:41  中亿丰数字科技  阅读(28)  评论(0)    收藏  举报