zf-fe

✍️ Tangxt ⏳ 2020-06-11 🏷️ JS 专题

20-综合专题之 THIS 的五种情况 1

★前言

专题课,用于汇总前边所学的内容,其中专题有:

  1. 专题一:JS 中 THIS 的五种情况
  2. 专题二:JS 中数据类型检测的四种方案(核心内容,分析 jQuery 源码的做法) -> 如果你能够把 jQuery 研究透了,那么你的水平可以有 p6+,即便你不会 vue 和 react……总之研究 jQuery 源码很考究你的 JS 功底 -> 老师在这个 JS 课里边主要会讲一些自认为比较好的 jQuery 源码 -> 目的是:提升自己的原生 JS 编程能力,以及其中用到的编程思想和技巧……
  3. 专题三:JS 中的四大继承方案 -> 继承方案有很多,但常用的就四种

讲完这三大专题后,就会讲浏览器的底层运行机制,如渲染机制,包括 DOM 的回流和重绘,以这个为依托,接下来会讲到 JS 的同步和异步编程、eventloop/eventqueue 等

我没有想过浏览器的底层运行机制居然是同步和异步编程等的前置知识……

★JS 中 THIS 的五种情况

◇概述

◇情况一:事件绑定

事件绑定一般有两种方式:

  1. DOM 0 级事件绑定,如 btn.onclick = function xxx(e) {}
  2. DOM 2 级事件绑定,如 btn.addEventListener('click', function xxx(){}, false)

DOM 结构如下:

<button id="btn">click me</button>

1、DOM 0 姿势

btn.onclick = function xxx() {
  console.log(this); //<button id="btn">click me</button>
};

如果你这样:

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

btn.onclick = function() {
  xxx();
};

那么 this 值就是 window 了 -> 因为我们在调用 xxx 的时候是直接裸调用的,即 xxx() 这样的!

小结:

给元素的某个事件行为绑定方法,事件触发,方法执行,此时方法中的 THIS 一般都是当前元素本身(一般:表示还存有特殊情况)

2、DOM 2 姿势

不兼容 IE6、7、8:

btn.addEventListener(
  "click",
  function xxx() {
    console.log(this); // -> btn 这个 dom 元素
  },
  false
);

兼容 IE6、7、8:

// <= IE8 浏览器中的 DOM2 事件绑定
btn.attachEvent("onclick", function xxx() {
  console.log(this); // -> window
});

而这就是所谓的「特殊情况」,一般我们都认为 this 是 btn ,但在 DOM2 姿势里边,为了兼容 IE6、7、8,所以可能会用到 attachEvent 这个 API,而该 API 不同于 addEventListener ,即当事件触发,执行 callback,而 callback 里边的 this 指向是 window ,而不是 btn

当然,就目前而言基本就不需要兼容 IE 了,所以这种特殊情况,基本可以无视之……

对了,还有一种特殊情况:

function xxx() {
  console.log(this); // -> window
}

btn.onclick = xxx.bind(window);

其中原理:

xxx.bind(window) 首先会返回一个匿名函数 (AM), 把 AM 绑定给事件;点击触发执行 AM,AM 中的 THIS 是元素,但是会在 AM 中执行 xxxxxx 中的 THIS 是预先指定的 WINDOW

简单来说,bind 是这样改变 this 指向的:

function bind(thisArg) {
  let _this = this
  return function anonymous() {
    _this.call(thisArg)
  }
}

◇情况二:普通函数执行

规律:

THIS2:普通函数执行,它里面的 THIS 是谁,取决于方法执行前面是否有“点”,有的话,“点”前面是谁 THIS 就是谁,没有则 THIS 指向 WINDOW(当然,严格模式下是 UNDEFINED)

测试:

function xxx() {
  console.log(this);
}
let obj = {
  name: "frank",
  xxx: xxx,
};
xxx(); // -> window
obj.xxx(); // -> obj

加深理解:

我们知道 hasOwnProperty 的定义是这样的:

Object.prototype.hasOwnProperty = function hasOwnProperty() {}

当我们这样做:

console.log(obj.hasOwnProperty("name"));
console.log(obj.__proto__.hasOwnProperty("name"));

那么结果:

hasOwnProperty方法中的 this 为 obj -> TRUE
hasOwnProperty方法中的 this 为 obj.__proto__(Object.prototype) -> FALSE

Object.prototype 旗下可没有 name 这个属性!

当然,如果你这样做:

Object.prototype.hasOwnProperty.call(obj, "name")

那么这等价于:

obj.hasOwnProperty('name')

补充一点:

像那种匿名的自执行函数,其 this 一般是 window


Q: `hasOwnProperty` 和 `in` 的用法?

这里的私有和公有是针对于是否可以访问原型链上的方法来说的……

console.log(obj.hasOwnProperty("name")); // =>true
console.log(obj.hasOwnProperty("toString")); // =>false
console.log("toString" in obj); // =>true

◇情况三:构造函数执行

规律:

THIS3:构造函数执行( new xxx ),函数中的this是当前类的实例

测试:

function Fn(name) {
  console.log(this);
  // => this.xxx = xxx 是给当前实例设置私有属性
  this.name = name || 'hi';
}
let f = new Fn("frank"); // -> this -> f实例
let x = new Fn; // -> this -> x实例

一些格式化工具,会把 let x = new Fn 写法格式化成 let x = new Fn()

◇情况四:箭头函数

规律:

THIS4:箭头函数中没有自身的THIS,所用到的THIS都是箭头函数作用域的上一级作用域中的THIS -> 或者说是定义箭头函数的上下文中的THIS

测试:

let xx = {
  name: "xx",
  fn: () => {
    // 该箭头函数作用域的上一级就是全局作用域!
    console.log(this); // -> window
  },
};

xx.fn();

简单来说,这个this就跟普通变量一样,当前作用域找不到,就顺着作用域链往上级找……

当然,箭头函数没有的东西还有很多:

  1. 它没有 prototype (也就是没有构造器),所以它不能被new执行
  2. 它没有arguments实参集合(可以基于…args剩余运算符获取)

如:

let xx = {
  name: "xx",
  fn: (...args) => {
    console.log(args); // -> [1,2,3]
    console.log(this, arguments); // 运行时错误:arguments is not defined
  },
  fx: function() {
    console.log(this, arguments);
  },
};
xx.fx(4, 5, 6);
xx.fn(1, 2, 3);

箭头函数的一些场景应用:


场景一:

有这么一段代码:

let obj = {
  name: "xx",
  fn: function() {
    console.log(this); // -> xx实例
    return function yy() {
      console.log(this); // -> window
      this.name = "obj";
    };
  },
};
let x = obj.fn();
x();

在我们知道x所在的上下文中的 thisobj 的情况,我们想让 x 执行的时候,改变 obj 的name属性值为 obj

简单来说,有两种做法:

①自己声明一个变量,如 _this ,然后把 x 里边的 this 改成是 _this

let obj = {
  name: "xx",
  fn: function() {
    console.log(this); // -> xx实例
    let _this = this;
    return function yy() {
      console.log(this); // -> window
      console.log(_this); // -> xx实例
      _this.name = "obj";
    };
  },
};
let x = obj.fn();
x();

②使用箭头函数

let obj = {
  name: "xx",
  fn: function() {
    console.log(this); // -> xx实例
    return () => {
      console.log(this); // -> xx实例
      this.name = "obj";
    };
  },
};
let x = obj.fn();
x();

场景二:

有这么一段代码,我们想让一个定时器在1s后改变 objname 属性值:

let obj = {
  name: "xx",
  fn: function() {
    console.log(this) // -> obj
    setTimeout(function() {
      console.log(this); // -> window
      this.name = "obj";
    }, 1000);
  },
};
obj.fn();

定时器的callback中的 thiswindow ,这一点就像是这样:

window.setTimeout(function() {
  console.log(this); // -> window
}, 1000);
btn.addEventListener(
  "click",
  function() {
    console.log(this); // -> btn
  },
  false
);

我们要让定时器中的 callback 中的 thisobj ,那么只需要让 callback 为箭头函数就行了:

let obj = {
  name: "xx",
  fn: function() {
    console.log(this); // -> obj
    setTimeout(() => {
      console.log(this); // -> obj
      this.name = "obj";
    }, 1000);
  },
};
obj.fn();

为啥会这样呢?因为JS是静态作用域的,而箭头函数是没有预设的this变量的,所有箭头函数在哪儿定义的,那么其里边的this变量就会顺着作用域链往哪儿找……


小结:

★总结