JS this 指向配合场景实战

我心飞翔 分类:javascript

前言

本文将通过一个个小案例,详细的讲解 this 在各种情况下所指向的内容是如何变化的。

章节一

/**
 * this 是 JavaScript 的关键字
 * 当前环境执行期上下文对象的一个属性
 * this 在不同的环境、不同作用下,表现是不同的
 * 
 * 获取全局对象
 * web:window、self、frames、this
 * node:global
 * worker:self
 * 通用:globalThis
 */

var a = 'global -> a';
var obj = {
  a: 'obj -> a',
  test: function () {
    console.log(this.a); // 调用当前方法的对象
    console.log(window.a); // web 环境下全局对象, node 环境下不存在
    // console.log(global.b); // node 环境下全局对象, web 环境下不存在
    console.log(globalThis.a);
  }
}
obj.test();


function useStrict() {
  'use strict' 
  return this;
}

/**
 * 使用严格模式的话,会返回 undefined,因为 useStrict() 没说明谁调用它
 * 但是 window.useStrict() 这样调用的话,严格模式也会返回 window,因为明确表明了谁调用了它
 * 
 * 使用非严格模式的话,都会返回 window
 */
console.log(useStrict())
console.log(window.useStrict())
 

章节二

class People {
  constructor() {
    // 将会定义到实例对象 this 的属性上 -> new -> this -> {}
    this.print = function () {
      console.log('实例属性:', this);
    }
  }

  // 类的原型上的方法 -> People.prototype
  // new -> this -> {} -> __proto__ -> People.prototye
  print() {
    console.log('类原型上的方法:', this);
  }

  // 类的静态方法
  static print() {
    console.log('静态属性');
  }
}

/** 
 * Class 其实就是 函数
 */
 
function People() {
  this.print = function () {
    console.log('实例属性:' + this);
  }
}

People.prototype.print = function () {
  console.log('类原型上的属性:' + this);
}

People.print = function () {
  console.log('静态属性:' + this);
}


const man = new People();

/**
 * 输出:实例属性: People {print: ƒ}
 * 这里需要知道的是为什么不是输出“类原型上的方法”,那是因为“类原型上的方法”在 People 在定义的时候就进行的赋予了
 * 而“实例属性”是在 new 的时候,对 constructor 的执行,并且改变 this 为实例对象的时候赋予的一个实例上的方法
 */
man.print();
People.print(); // 输出:静态属性
 

章节三

const PeopleA = Object.create(null); // 输出的对象,没有 __proto__
const PeopleB = Object.create({ a: 1 }); // 输出的对象的 __proto__ 指向了传入的 {a: 1}


class Father {
  constructor(age) {
    this.age = age;
  }

  swim() {
    console.log('Go swimming!!!');
  }
}

class Son extends Father{
  constructor() {
    /**
     * 类似于 call 的继承:在这里 super 相当于把 A 的 constructor 给执行了,
     * 并且让方法中的 this 是 B 的实例,super 当中传递的实参都是在给 A 的 constructor 传递。
     * super(18) 相当于 Father.prototype.constructor.call(this, 18)
     * super.swim() 相当于 Father.prototype.swim()
     */
    super(18);
    this.hooby = 'traval';
    console.log(this.age);
  }

  study() {
    console.log(this);
    this.swim();
  }
}
const son = new Son();

son.study();

// 需要注意,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。
 

章节四

var obj = {
  a: 1
}

var obj2 = {
  a: 100
}

var a = 2;

function test(b, c, d, e, f) {
  // this 默认 -> 全局对象 window
  console.log(this.a, b, c, d, e, f);
}

test(); // 2 undefined undefined undefined undefined undefined
test.call(obj); // 1 undefined undefined undefined undefined undefined
test.apply(obj); // 1 undefined undefined undefined undefined undefined
test.call(obj, 3, 4); // 1 3 4 undefined undefined undefined
test.apply(obj, [3, 4]); // 1 3 4 undefined undefined undefined

var test1 = test.bind(obj, 3, 4);

test1(); // 1 3 4 undefined undefined undefined

var test2 = test1.bind(obj2, 5, 6);

test2(7); // 1 3 4 5 6 7

/**
 * 这里的 test2 输出为 1 3 4 5 6 7 为什么呢?
 * 首先 bind 绑定的时候会返回一个新的函数 test2,
 * 其次,bind 内部也是使用 call 来改变 this 指向。在 test1 的时候执行, 内部 test 指向定义为 obj
 * test1.bind() 的执行的时候,返回 test2,test2 执行的时候,内部 test1 的 this 会指向 obj2,
 * 随后 test1 执行,内部的 test 的 this 指向还是 obj,所以 this.a 为 1,至于参数会不断透传下去
 */
 

章节五

'use strict'
const test = () => {
  console.log(this);
}
function test1() {
  console.log(this);
}
const test2 = function () {
  console.log(this);
}
test(); // window
test1(); // undefined
test2(); // undefined

// 严格模式下,箭头函数绑定了 window -> this
 

章节六

var obj = {
  a: 1
}

var a = 2;

const test = () => {
  console.log(this.a);
}

test(); // 2
test.call(obj); // 2
test.apply(obj); // 2
var test1 = test.bind(obj);
test1(); // 2

new test(); // test is not constructor

obj.test = () => {
  console.log(obj);
  console.log(this);
}
obj.test(); // obj/window

/**
 * 箭头函数是忽略任何形式的 this 指向改变 
 * 箭头函数一定不是一个构造函数
 * 箭头函数不是谁调用 this 就会指向谁
 */
 

章节七

obj.test = function () {
  const t1 = () => {
    console.log(this);
  }
  t1();
}
obj.test(); // obj

obj.test = () => {
  const t1 = () => {
    console.log(this);
  }
  t1();
}
obj.test(); // window

obj.test = function() {
  const t1 = () => {
    // t1 是箭头函数 this -> obj
    const t2 = () => {
      console.log(this);
    }
    t2();
  }
  t1();
}
obj.test(); // obj

obj.test = function() {
  const t1 = function() {
    const t2 = () => {
      // t1 是普通函数 this -> window
      console.log(this);
    }
    t2()
  }
  t1();
}
obj.test(); // window

/**
 * 总结:
 * 箭头函数 this 总是指向外层非箭头函数的 this 指向
 * 箭头函数是忽略任何形式的 this 指向改变 
 * 箭头函数一定不是一个构造函数
 * 箭头函数不是谁调用 this 就会指向谁
 */
 

章节八

// test3 输入变量赋值一个函数,这个在执行的时候才会进行声明和赋值,所以必须写在前面
const test3 = () => {
    console.log(this);
}

var obj = {
    a: 1,
    b: 2,
    test: function() {
        console.log(this.a);
    },
    test2: test2,
    test3: test3,
    c: {
        d: 4,
        test4: function () {
            console.log(this.d);
        }
    },
    test5: function () {
        function test6() {
            console.log(this);
        }
        test6();
    }
}

obj.__proto__ = {
    e: 20
}

// 预编译的时候函数 test2 就声明了和定义了
function test2() {
    console.log(this.b);
}

obj.test(); // 1
obj.test2(); // 2
obj.test3(); // window
obj.c.test4(); // 4 这里按照对象中的 function 的最近谁调用 this 就指向那个宿主的原则即可了解
obj.test5(); // window, 要知道的是 test6 的调用寄主就是 window·
console.log(obj.e); // 20

var obj2 = Object.create({
    test0: function () {
        console.log(this.a + this.b);
    }
});

obj2.a = 1;
obj2.b = 2;

obj2.test0(); // 1 + 2 = 3 这里的 this 指向就是 obj2

/**
 * 总结:
 * this 的指向的基本原则:谁是调用 this 的寄主,this 就指向谁
 * 另类的就是对于箭头函数不同,箭头函数内部 this 的指向为最近外层非箭头函数的作用域
 */
 

章节九

// 使用的是 function Object() {} 构造函数构造的
var obj1 = {
    a: 1,
    b: 2
}

// 使用 Object.create() 进行构造,可传入一个 prototype 对象, 传入 null 为一个无 prototype 对象
var obj2 = Object.create({
    a: 1,
    b: 2
});

// 使用 Object.defineProperty 进行属性定义
var obj3 = {};
Object.defineProperty(obj3, 'a', {
    get() {
        console.log(this)
        return 1;
    }
})
console.log(obj3.a); // 这里输出 obj3
// 这里 this 指向的就是 obj3

function Test() {
    this.a = 1;
    this.b = 2;
    console.log(this);

    // return this; // 作为构造函数,默认行为范围 this
    return {
        a: 3,
        b: 4
    }
    // return null; // 作为构造函数,也是返回 this
    // return undefined; // 作为构造函数,也是返回 this
    // return 非 object 也返回 this
}

var obj4 = new Test(); // obj4 -> { a: 3, b: 4 }、console.log(this) -> Test {a: 1, b: 2}
console.log(obj4);
/**
 * new 过程:
 * var _obj = {};
 * _obj.__proto__ == Test.prototype;
 * var res = Test.call(_obj);
 * return typeof res === 'object' && res !== null ? res : obj;
 * 
 * 总结:
 * 构造函数默认隐式返回 this,或者手动返回 this,这个 this 指向的新对象的构造都是成功的
 * 如果手动返回一个新对象,那么这个 this 指向的那个对象将会被忽略,失效掉,因为失去了引用,相当于没有 new,比如如下:
 */

 var obj5 = new Test(); // { a: 3, b: 4 }
 var obj6 = Test(); // { a: 3, b: 4 }
 

章节十

var oBtn1 = document.getElementById('btn1');

oBtn1.onclick = function () {
  console.log(this); // 输出了:<button id="btn1">click</button>
}

oBtn1.addEventListener('click', function () {
  console.log(this); // 输出了:<button id="btn1">click</button>
}, false);

// 事件处理函数内部的 this 总是指向被绑定 DOM 元素

!(function (doc) { 
  var oBtn2 = doc.getElementById('btn2');

  function Plus() {
    this.a = 1;
    this.b = 2;
    this.init();
  }

  Plus.prototype.init = function () {
    this.bindEvent();
  }

  Plus.prototype.bindEvent = function () {
    oBtn2.addEventListener('click', this.handleBtnClick, false);
  }

  Plus.prototype.handleBtnClick = function () {
    console.log(this); // <button id="btn2">+</button>
    console.log(this.a + this.b); // NaN
  }

  window.Plus = Plus;
})(document)

var plus = new Plus();
/**
 * 这里点击 btn2 事件,handleBtnClick 内部的 this 为 dom 元素本身
 * 如果需要内部 this 为 Plus 实例,可有以下方式:
 */
// 方式一
Plus.prototype.bindEvent = function () {
  oBtn2.addEventListener('click', this.handleBtnClick.bind(this), false);
}
// 方式二
function Plus() {
  this.a = 1;
  this.b = 2;
  this.handleBtnClick = this.handleBtnClick.bind(this);
  this.init();
}
// 方式三
Plus.prototype.bindEvent = function () {
  oBtn2.addEventListener('click', () => this.handleBtnClick(), false);
}

/**
 * 总结:
 * DOM 事件处理函数内部的 this 总是指向被绑定 DOM 元素
 */
 

章节十一

/**
 * 类中是严格模式
 */

 /**
  * 父亲有一个吃水果的方法 还有一个水果
  * 儿子有自己的水果 -> 儿子使用父亲吃水果的方法吃自己的水果
  */

class Father {
  // constructor() {
  //   this.eat = this.eat.bind(this); // bind 会重新返回一个匿名函数
  // }

  get fruit() {
    return 'apple';
  }

  eat() {
    console.log('I am eating an ' + this.fruit);
  }
}

class Son {
  get fruit() {
    return 'orange';
  }
}

var father = new Father();
var son = new Son();

son.eat = father.eat;

father.eat(); // I am eating an apple
son.eat(); // I am eating an orange

/** 
 * 如何让 son 也吃父亲的水果内?
 * 其实就是把父亲的 eat 内部绑定住父亲的 this
 * 可以这么做,给 Father 增加一个构造函数,如下:
 * constructor() {
 *  this.eat = this.eat.bind(this); // bind 会重新返回一个匿名函数
 * } 
 */
 

总结

总结:

  1. this 是 JavaScript 的关键字,当前环境执行期上下文对象的一个属性,this 在不同的环境、不同作用下,表现是不同的
  2. 获取全局对象
    1. web:window、self、frames、this
    2. node:global
    3. worker:self
    4. 通用:globalThis
  3. 扩展:Class 中由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的
  4. 扩展:Object.create(null); // 输出的对象,没有 proto
  5. 扩展:Object.create({ a: 1 }); // 输出的对象的 proto 指向了传入的 {a: 1}
  6. 使用 bing、call、apply 的时候会变更非箭头函数内的 this 指向
  7. 箭头函数 this 总是指向外层非箭头函数的 this 指向
  8. 箭头函数是忽略任何形式的 this 指向改变
  9. 箭头函数一定不是一个构造函数
  10. 箭头函数不是谁调用 this 就会指向谁
  11. 对象中的 function 的最近谁调用 this 就指向那个宿主的原则
  12. this 的指向的基本原则:谁是调用 this 的寄主,this 就指向谁
  13. 对于箭头函数不同,箭头函数内部 this 的指向为最近外层非箭头函数的作用域
  14. 构造函数默认隐式返回 this,或者手动返回 this(返回 null, undefined 也是返回 this),这个 this 指向的新对象的构造都是成功的
  15. DOM 事件处理函数内部的 this 总是指向被绑定 DOM 元素

回复

我来回复
  • 暂无回复内容