【JS】this对象详解及案例分析

执行环境的解释

执行环境(execution context)是JS中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然编写代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

(全局执行环境、执行环境的执行与销毁)
全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出-例如关闭浏览器或网页-才会被销毁)。

(执行流)
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个机制控制着。

(变量对象,作用域链,前端向后)
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终是当前执行代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中不存在)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链的最后一个对象。

(标识符解析)
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致报错)

四种绑定方式

默认绑定
隐式绑定
显式绑定
new 绑定

优先级

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

判断this指向的方法

(这里借鉴大牛的总结)

第1步: 查看函数在哪被调用。
第2步: 点左侧有没有对象?如果有,它就是 “this” 的引用。如果没有,继续第 3 步。
第3步: 该函数是不是用 “call”、“apply” 或者 “bind” 调用的?如果是,它会显式地指明 “this” 的引用。如果不是,继续第 4 步。
第4步: 该函数是不是用 “new” 调用的?如果是,“this” 指向的就是 JavaScript 解释器新创建的对象。如果不是,继续第 5 步。
第5步: 是否在“严格模式”下?如果是,“this” 就是 undefined,如果不是,继续第 6 步。
第6步: JavaScript 很奇怪,“this” 会指向 “window” 对象

案例详解

function Log(back) {
  console.log(back)
}

简化一下控制台打印

let length = 10;
let fn = function() {
  Log(this);
  return length;
}
let arr = [fn];
Log(arr[0]());

在这里插入图片描述
相当于第二步,arr数组调用,故this指向arr数组;
函数有返回值,length变量,在全局作用环境中有声明,故输出10;

// var length = 10;
let length = 10;
let fn = function() {
  Log(this);
  Log(this.length);
  return length;
}.bind(this);
let arr = [fn];
Log(arr[0]());

在这里插入图片描述
在这里插入图片描述

相当于第三步,通过bind方法,显式声明this的指向,这个时候,this指向Window对象;
this.length就变成了Window.length,刚好它有length属性,默认就是0;
注意: let声明改成var声明,结果有不一样了!
输出结果: 10

这里引入一个ES6新概念: 顶层对象的属性
在这里插入图片描述
全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块
1.函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。
2.不管是严格模式,还是普通模式,new Function(‘return this’)(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval、new Function这些方法都可能无法使用。

const user = {
  name: 'Tyler',
  age: 27,
  greet() {
    Log(this);
    Log(`Hello, my name is ${this.name}`)
  },
  mother: {
    name: 'Stacey',
    greet() {
      Log(this);
      Log(`Hello, my name is ${this.name}`)
    }
  }
}
user.greet();
user.mother.greet();

在这里插入图片描述
相当于第二步: 点左侧是个对象,this就是它的引用;

function greet() {
  Log(`Hello, my name is ${this.name}`)
}
const user = {
  name: 'Tyler',
  age: 27
}
greet();
greet.call(user);

在这里插入图片描述
在这里插入图片描述
相当于第六步:直接执行函数,指向window对象,window.name === ‘’;
相当于第三步: call方法调用,this显式地指向user;

function greet (lang1, lang2, lang3) {
    Log(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
  }

  const user = {
    name: 'Tyler',
    age: 27,
  }

  const languages = ['JavaScript', 'Ruby', 'Python']

  greet.call(user, languages[0], languages[1], languages[2]);
  // greet.apply(user, languages);
  // const newFn = greet.bind(user, languages[0], languages[1], languages[2]);
  // newFn();

在这里插入图片描述
第三步: 使用call,apply,bind方式调用;
语法解释: 重点就是,记住传参格式
在这里插入图片描述
call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:
call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面;
apply 的所有参数都必须放在一个arguments数组里面传进去;
bind 除了返回是函数以外,它 的参数和 call 一样,使用时再调用;

function User(name, age) {
  console.table(this);
  Log(this);
  this.name = name;
  this.age = age;
}
const user = new User('Tyler', 27);
Log(user);

在这里插入图片描述
new操作符,底层会新创建一个对象,this指向这个新对象
我们模拟new的执行过程:

function New(Constructor, ...args){
  let obj = {};
  Object.setPrototypeOf(obj, Constructor.prototype);
  return Constructor.apply(obj, args) || obj;
}

function Foo(a){
    this.a = a;
}

Log(New(Foo, 1));

在这里插入图片描述
步骤:

  1. 创建一个空对象 obj;
  2. 将新创建的空对象的隐式原型指向其构造函数的显示原型;
  3. 使用 call 改变 this 的指向,如果无返回值或者返回一个非对象值,则将 obj 4. 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象;
function sayAge () {
  Log(this);
  Log(`My age is ${this.age}`)
}
sayAge();

function User() {
  this.name = 'John';
  setTimeout(function greet() {
    Log(this);
    Log(`Hello, my name is ${this.name}`);
  }, 1000);
}
const user = new User();

下面对 setTimeout 相关分析:
在这里插入图片描述
setTimeout并没有指向new 新生成的User对象,它的执行就与上面sayAge时一样的,下面有几种方式让它指向User;

function User() {
  this.name = 'John';
  setTimeout(function greet() {
    Log(this);
    Log(`Hello, my name is ${this.name}`); 
  }.call(this), 1000);
}
const user = new User();

call方式调用

function User() {
  this.name = 'John';

  setTimeout(function greet() {
    Log(this);
    Log(`Hello, my name is ${this.name}`); 
  }.apply(this), 1000);
}
const user = new User();

apply方式调用

function User() {
 this.name ='John';

 setTimeout(function greet() {
   Log(this);
   Log(`Hello, my name is ${this.name}`); 
 }.bind(this), 1000);
}
const user = new User();

bind方式调用,可以不用加()执行符号,第一个参数本来就是一个函数;

function User() {
  this.name = 'John';
  const self = this;
  setTimeout(function greet() {
    Log(self);
    Log(`Hello, my name is ${self.name}`); 
  }, 1000);
}
const user = new User();

利用闭包,虽然User函数已经执行完毕,但self变量被greet使用,故greet函数仍然保持对上一级执行环境的访问权,直到执行完成,才会被销毁;

function User() {
  this.name = 'John';
  setTimeout(function greet(self) {
    Log(self);
    Log(`Hello, my name is ${self.name}`); 
  }, 1000, this);
}
const user = new User();

利用setTimeout本身传参的特性,把this传入setTimeout执行函数;

function User() {
  this.name = 'John';
  setTimeout(() => {
    Log(this);
    Log(`Hello, my name is ${this.name}`); 
  }, 1000);
}
const user = new User();

利用ES6的箭头函数;
为了避免 ES5 中使用 this 的坑。箭头函数的 this 始终指向函数声明时的 this,而非执行时。箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined
在这里插入图片描述

function foo() {
  Log(this);
  Log(this.a);
}
let a = 2;
// var a = 2;
foo();

let obj1= {
  a: 1,
  foo
};

obj1.foo();

在这里插入图片描述
ES6变量声明
为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性
在这里插入图片描述
故第二次打印的结果是undefined;

// new 绑定 > 显式绑定
function foo(a) {
    this.a = a;
}

let obj1 = {};

let bar = foo.bind(obj1);
bar(2);
Log(obj1);

let obj2 = new bar(3);
Log(obj1);
Log(obj2);

在这里插入图片描述
最后2次结果可以看出,new是新创建了foo对象;

最后看一个比较经典的案例

df
const boss = {
  name: 'boss',
  returnThis () { // 需要被调用的函数 returnThis
    return this
  }
};
Log(boss.returnThis() === boss);

const boss1 = {
  name: 'boss1',
  returnThis () {
    return boss.returnThis(); // 跟踪,执行时的调用, .左边是boss,故方法的this指向的就是boss对象
  }
};
Log(boss1.returnThis()); // 执行时的调用

const boss2 = {
 name: 'boss2',
 returnThis() {
   const returnThis = boss.returnThis; // 只是一个函数 function returnThis() {return this}
   return returnThis(); // 这才是执行时的调用,没有. 没有显式调用 没有new等,所以是指向Window
 }
};
Log(boss2.returnThis()); // 执行时的调用

const boss3 = {
  name: 'boss3',
  returnThis: boss.returnThis // function returnThis() {return this}
}
Log(boss3.returnThis());

在这里插入图片描述

总结

此类问题,以后都不再是问题!!!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值