前言
一个系统/程序/页面的执行状态分为
静态创建阶段和动态执行阶段。静态创建阶段会进行代码分析或者说是结构分析,比如说作用域链的确定(当前变量和所有的父级变量)、变量声明(参数、变量、函数)。动态执行阶段执行的第一个就是this,this指向当前环境的上下文,第二个就是变量的使用,建立一个引用关系。第三个是函数的引用,函数在动态执行过程中,在动态执行阶段后才能区别或者拿到返回值,还可以去判断当前函数的作用域是局部作用域还是全局作用域。
课程目标
- 作用域、作用域链
- 执行上下文
- 立即执行、块级作用域
知识要点
作用域 作用域链
作用域
作用域和执行上下文是息息相关的,作用域就是执行上下文,值和表达式在其中可见或可被访问到的上下文。
简单来说,作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
function fn(){
var scope = "作用域";
console.log(scope)
}
fn() // 作用域
console.log(scope) // error
作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。 作用域链是一种向上查找的关系,白话概述就是儿子可以用爸爸的东西。
let a = "grandpa";
console.log(a);
function father(){
let b = "father";
console.log(b);
son();
function son(){
let c = "son";
console.log(c);
console.log(a) // 作用域向上查找
}
}
father()
为什么函数的执行可以在函数声明之前而不会报错?
函数提升
fn();
function fn(){ // 会提升到当前作用域的最顶端
console.log(1)
}
函数有函数提升,那么变量也会有变量提升吗?
变量提升
console.log(a);
var a = "a";
// ===
var a;
console.log(a);
a = "a";
在变量提升中有两个例外就是ES6新增的let/const关键字。
暂时性死区(TDZ)
console.log(a); // error
// let a = "a";
const a = "a";
在使用let和const声明之前去使用(读写操作)就会出现暂时性死区。
ES6规定,
let/const命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错。总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区( temporal dead zone,简称TDZ)。
变量提升和函数提升的优先级
var fn = "fn"
function fn(){}
console.log(fn) // "fn"
在JavaScript中函数是第一公民,函数提升的优先级大于变量提升。
块级作用域
整个作用域链归根结底就是向上查找和优先级的问题,随之而来的就是块级作用域,
块级作用域存在的意义就在于限制变量只能在这个块之内。
if(true){
let a = 1;
var b = 2;
console.log(a,b) // 1 2
}
console.log(a) // error
console.log(b) // 2
总结
在我们目前的代码编写过程中:
- 对于作用域链,我们直接通过创建态来定位作用域链
- 手动取消全局,使用块级作用域
this 上下文 context
this是在执行时动态读取上下文决定的,而不是创建时,重点在于当前this的指向。
函数直接调用
function foo(){
console.log("函数内部的this",this)
}
foo() // window
foo() === window.foo() // true
在当前环境下,foo是在全局环境下指向,并且没有表明被任何人调用,这里隐式绑定的就是window。在基础岗面试中函数的直接调用会被演化为函数表达式、匿名函数、嵌套函数。
隐式绑定
function fn(){
console.log("隐式绑定",this.a);
}
const obj = {
a:1,
fn // fn:fn 在ES6中,如果函数的属性和值相同,可以缩写成这样
}
obj.fn();
在上述代码中,在fn函数执行的过程中,它已经不在是全局下的孤立函数,它的this/上下文做了赋值和绑定,在obj中引用了fn函数,它指向的就是obj对象。
隐式绑定的时候,this指向的是调用的上一级,这种情况通常会存在于对象,数组,等引用关系逻辑之上。 举个例子:
const foo = {
bar:100,
fn:funciton(){
console.log(this.bar);
console.log(this)
}
}
//取出
const fn1 = foo.fn;
fn1();
在上诉代码中,foo.fn被拿出来了,同时被fn1引用,作为一个全局下的独立函数,这时this的指向是window。
如何改变this指向
const o1 = {
text:"o1",
fn:function (){
// 直接使用上下文,领导直接分活
return this.text
}
}
const o2 = {
text:"o2",
fn:function (){
// 呼叫了隔壁组o1的leader协助执行 - 部门协助
return o1.fn()
}
}
const o3 = {
text:"o3",
fn:function (){
// 临时构造,没有直接的上下文,直接指向全局环境
const fn = o1.fn;
return fn()
}
}
console.log(o1.fn(),"o1fn"); // o1 o1fn
console.log(o2.fn(),"o2fn"); // o1 o2fn
console.log(o3.fn(),"o3fn"); // undefined o3Fn
在执行函数时,函数被上一级调用,去找发起方。
直接变成公共执行时,指向的是window全局。
问题:如何使o2.fn()输出
o2? 不人为干涉:引用赋值。
人为干涉:使用call/bind/apply改变this指向。
const o1 = {
text:"o1",
fn:function (){
return this.text
}
}
const o2 = {
text:"o2",
fn:o1.fn
}
console.log(o1.fn(),"o1fn"); // o1 o1fn
console.log(o2.fn(),"o2fn"); // o2 o2fn
call/apply/bind
区别:call/apply 传参不同 依次传入/数组传入,bind和call/apply 返回函数体,需要执行。
call
const obj = {
a:1,
b:2
}
function fn(){
console.log(this.a,this.b,...arguments) // arguments是一个类似于数组的对象,对应于传递给函数的参数
}
fn.call(obj,3,4,5) // 1,2,3,4,5
在上诉代码中,我们可以看到使用call来改变this指向,指向obj的同时传递三个参数,在函数中我们可以使用arguments对象来接收,argument对象是隐式的,我们不能手动的创建。
对于原理或手写类题目,我们首先要清晰的是解题思路,第一步就是call有什么用?需要放在什么地方或者说挂载在哪里?第二部就是把你的思路转换成代码实现。
Function.prototype._call = function(context,...args){
context = (context === null || context === undefined) ? window : new Object(context);
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result
}
分析代码,首先我们创建了一个context上下文,...args对剩余参数进行解构,接着我们对context进行边缘检测(如果传进来的对象不存在,则this指向全局window),然后我们给context添加fn属性并引用当前调用call的函数。例如 fun.call,我们当前的this就指向fun。为了使我们的call可以被多次调用,我们创建一个变量进行接收,方便后续返回,避免占用内存,我们选择当函数执行完之后去清除这个属性。
apply
const obj = {
a:1,
b:2
}
function fn(){
console.log(this.a,this.b,...arguments)
}
fn.apply(obj,[3,4,5]) // 1,2,3,4,5
与call方法类似,call方法接收的是一个参数列表,而apply方法接收的是一个包含多个参数的数组。接收除了object、array、function、null之外的参数会出现报错。
Function.prototype._apply = function (context, args) {
if (typeof args === "array" || typeof args === "object" || typeof args ==="function"|| typeof args ==="null") {
context = context === undefined || context == null ? window : new Object(context);
context.fn = this;
const result = args ? context.fn(...args) : context.fn()
delete context.fn;
return result
} else {
throw "TypeError: CreateListFromArrayLike called on non-object"
}
}
bind
const obj = {
a:1,
b:2
}
function fn(){
console.log(this.a,this.b,...arguments)
}
let fun = fn.bind(obj,3,4,5)
fun(6,7) // 1,2,3,4,5,6,7
在 bind 被调用时,这个新函数的 this 被指定为 bind 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
Function.prototype._bind = function(context,...args1){
const _this = this;
return function(...args2){
return _this.call(context,...args1,...args2)
}
}
new this指向
class Course {
constructor(name) {
this.name = name;
console.log('构造函数中的this:', this);
}
test() {
console.log('类方法中的this', this);
}
}
const course = new Course('this');
course.test();
在构造函数中,this指向的是创建的实例对象。
构造函数中异步方法的this指向
class Course {
constructor(name) {
this.name = name;
console.log('构造函数中的this:', this);
}
test() {
console.log('类方法中的this', this);
}
asyncTest() {
console.log('异步方法外:', this);
setTimeout(function() {
console.log('异步方法内', this);
}, 500)
ajax.get().then(res => {
// 异步方法
})
}
}
const course = new Course('this');
course.test();
course.asyncTest();
异步中的this指向的是去全局window,为什么? 异步函数创造的是一个
独立的作用域,异步函数之内就相当于在执行栈之外,因为我们的JavaScript是单线程,所有的异步都是在消息队列里,当我们的当前执行栈执行完之后,才会去调用消息队列里的东西,在执行消息队列的时候,我们的上下文已经消失了。 执行setTimeout,匿名函数执行上下文,在队列中和全局执行函数效果相同 - 指向window。
如何解决异步方法指向问题
- 记录this
- 显式
- 箭头函数
闭包
概念:一个函数和它周围状态的引用捆绑在一起的组合。
// 函数作为返回值的场景
function mail() {
let content = '信';
return function() {
console.log(content);
}
}
const envelop = mail();
envelop();
- 函数可以作为返回值传递的(在上述bind已经说明了)
- 函数外部可以通过一定方式获取到内部局部作用域的变量 => 导致内部局部变量不能被GC(垃圾回收)
// 函数作为参数传递
// 单一职责
let content;
// 通用存储
function envelop(fn) {
content = 1;
fn()
}
// 业务逻辑
function mail() {
console.log(content);
}
envelop(mail);
函数嵌套
let counter = 0;
function outerFn() {
function innerFn() {
counter++;
console.log(counter);
}
return innerFn;
}
outerFn()();
可以通过外层函数去调用内层的函数。什么意思? 当我们想把核心的代码通过函数拆解成若干个微型的方法,通过一个大的逻辑外壳去包裹,而外部只用关心逻辑外壳给我们提供的方法去调用就可以了,并不知道也并不关心里面有多少个方法,这就是
高内聚。
// 事件处理(异步)的闭包
let lis = document.getElementsByTagName('li');
for(var i = 0; i<lis.length; i++) {
(function(i) {
lis[i].onclick = function() {
console.log(i);
}
})(i);
}
立即执行函数
作用:可以让内部的方法/变量生成一个独立的作用域,同时也可以让外界无感知。
(function immediateA(a) {
return (function immediateB(b) {
console.log(a);
})(1);
})(0);
立即执行函数 / 立即执行嵌套 => 拥有独立作用域,立即执行函数就是JavaScript早期模块化的基础。
私有变量
function createStack() {
return {
items: [], // 外部可以更改
push(item) {
this.items.push(item);
}
}
}
const stack = {
items: [], // 内部维护
push: function() {}
}
function createStack() {
const items = [];
return {
push(item) {
items.push(item);
}
setItems() {},
getItems() {}
// 通过内部提供的set和get让用户读写,可以加于限制
}
}