JS面向对象:this全解

文章目录

  • 声明函数的四种方式
  • 如何区分执行主体
    • 事件绑定
    • 普通方法执行
    • 构造函数执行
    • 箭头函数执行
      • 定时器中的匿名回调函数
    • 隐式的this
  • 手动改变this
  • call
    • call 原理
    • call应用:把类数组转换为数组
    • 手写call
      • 优化
    • call 的深层理解
  • bind
    • 手写bind
  • apply
    • apply应用:获取数组中的最大值
  • 例题
    • 1
    • 2
    • 3

  • 全局上下文中的THIS是WINDOW;
  • 块级上下文中没有自己的THIS,它的THIS是继承所在上下文中的THIS的;
  • 在函数的私有上下文中,THIS的情况会多种多样
  • THIS不是执行上下文(EC才是执行上下文),THIS是执行主体

声明函数的四种方式

let f1 = new Function("x", "y", "return x + y");
function f2 (){};
let f3 = function() {};
let f4 = (x, y) => x + y;
  • 前三种支持this/arguments/new
  • 最后一种f4是es6新语法,不支持this/arguments/new
  • f4箭头函数中的this就是该函数声明所在的环境(上下文)

如何区分执行主体

  1. 事件绑定:给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的THIS是当前元素本身(特殊:IE6~8中基于attachEvent方法实现的DOM2事件绑定,事件触发,方法中的THIS是WINDOW而不是元素本身)
  2. 普通方法执行(包含自执行函数执行、普通函数执行、对象成员访问调取方法执行等):只需要看函数执行的时候,方法名前面是否有“点”,有“点”,“点”前面是谁THIS就是谁,没有“点”THIS就是WINDOW[非严格模式]/UNDEFINED[严格模式]
  3. 构造函数执行new Func:构造函数体中的THIS是当前类的实例
  4. ES6中提供了箭头函数: 箭头函数没有自己的THIS,它的THIS是继承所在上下文中的THIS
  5. 可以基于CALL/APPLY/BIND等方式,强制手动改变函数中的THIS指向:这三种模式是和直接很暴力的(前三种情况在使用这三个方法的情况后,都以手动改变的为主)

事件绑定


    // 事件绑定 DOM0  DOM2
    let body = document.body;
    body.onclick = function () {
        // 事件触发,方法执行,方法中的THIS是BODY
        console.log(this);
    };
    body.addEventListener('click', function () {
        console.log(this); //=>BODY
    });
    // IE6~8中的DOM2事件绑定
    box.attachEvent('onclick', function () {
        console.log(this); //=>WINDOW
    });

普通方法执行

前面没有执行主体,this就默认指向window

    (function () {
        console.log(this); //=>window
    })();
    let obj = {
        fn: (function () {
            console.log(this); //=>window
            return function () { }
        })() //把自执行函数执行的返回值赋值给OBJ.FN
    };

let obj = {
    fn: (function () {
    	// 自执行函数 this => window
        return function () {
            console.log(this);
        }
    })()
};
obj.fn();	// obj
let fn = obj.fn;
fn();	// window	

原型链中的this

[].slice(); //=>数组实例基于原型链机制,找到ARRAY原型上的SLICE方法([].slice),然后再把SLICE方法执行,此时SLICE方法中的THIS是当前的空数组
Array.prototype.slice(); //=>SLICE方法执行中的THIS:Array.prototype
[].__proto__.slice(); //=>SLICE方法执行中的THIS:[].__proto__===Array.prototype

构造函数执行

构造函数体中的THIS在“构造函数执行”的模式下,是当前类的一个实例,并且THIS.XXX=XXX是给当前实例设置的私有属性

function Func() {
    this.name = "F";
    console.log(this); 
}
Func.prototype.getNum = function getNum() {
    // 而原型上的方法中的THIS不一定都是实例,主要看执行的时候,“点”前面的内容
    console.log(this);
};
let f = new Func;
f.getNum();
f.__proto__.getNum();
Func.prototype.getNum();

箭头函数执行

不建议乱用箭头函数(部分需求用箭头函数还是很方法便的)

let obj = {
    func: function () {
        console.log(this);
    },
    sum: () => {
        console.log(this);
    }
};
obj.func(); //=>THIS:OBJ
obj.sum(); //=>THIS是所在上下文(EC(G))中的THIS:WINDOW
obj.sum.call(obj); //=>箭头函数是没有THIS,所以哪怕强制改也没用  THIS:WINDOW

定时器中的匿名回调函数

回调函数中的THIS一般都是WINDOW(但是有特殊情况)

解决方法
1、用于保存this的变量_this

let obj = {
	i: 0,
	// func:function(){}
	func() {
		// THIS:OBJ
		let _this = this;
		setTimeout(function () {
			// THIS:WINDOW 回调函数中的THIS一般都是WINDOW(但是有特殊情况)
			_this.i++;
			console.log(_this);
		}, 1000);
	}
};
obj.func();

2、基于bind把函数中的this预先处理

let obj = {
	i: 0,
	func() {
		setTimeout(function () {
			// 基于BIND把函数中的THIS预先处理为OBJ
			this.i++;
			console.log(this);
		}.bind(this), 1000);
	}
};
obj.func();

3、箭头函数中没有自己的THIS,用的THIS是上下文中的THIS,也就是obj

let obj = {
	i: 0,
	func() {
		setTimeout(() => {
			this.i++;
			console.log(this);
		}, 1000);
	}
};
obj.func(); 

隐式的this

js会自动给你传入this

fn(1, 2) === fn.call(undefined, 1, 2);
obj.method("hi") === obj.method.call(obj, "hi");
arr[0](1, 2); === arr[0].call(arr, 1, 2)

例如

let fn = function(x, y){console.log(this)};
fn(1, 2)

let obj = {
  fn
}
obj.fn(1, 2)	// 

let arr = [fn, 2];
arr[0](1,2);

在这里插入图片描述

手动改变this

call

call 原理

let res = fn.call(obj, 10, 20);
console.log(res);
  1. fn首先基于__proto__找到Function.prototype.call,并且让call方法执行
  2. 在call方法执行的过程中(call方法中的this->fn),把fn执行,并且让fn中的this变为传递的第一个参数obj,
  3. 再并且把10/20当做实参传递给fn,最后接收fn执行的返回值,把返回值作为call方法的返回值返回

call应用:把类数组转换为数组

  • arguments虽然是类数组,但是结构和数组一样(除了__proto__不是Array.prototype)
  • 所以操作数组的代码和操作arguments基本一致的(尤其是循环这种东西)
  • 如果我能让ARRAY原型上的slice执行,让方法中的this变为arguments,相当于把arguments转换为一个数组
  • 只要两个实例结构类似,那么大部分操作操作的他们方法都可以公用,无外乎就是THIS指向的问题

类数组操作

    var arr = [];
    for (var i = 0; i < arguments.length; i++) {
        arr.push(arguments[i]);
    }
    return arr;

让slice方法中的this变为arguments

    [].slice.call(arguments)
    Array.prototype.slice.call(arguments)
    let utils = (function () {
        function toArray() {
            return [].slice.call(arguments);
        }
        return {
            toArray
        };
    })();
    let ary = utils.toArray(10, 20, 30); //=>[10,20,30]
    console.log(ary);
    ary = utils.toArray('A', 10, 20, 30); //=>['A',10,20,30]
    console.log(ary);

使用forEach

[].forEach.call(arguments, item => {})

手写call

如何让fn中的this变为obj => obj.fn() => 需要保证fn函数作为obj某个成员的属性值

思路:把函数作为要改变的THIS对象的一个成员,然后基于对象的成员访问执行函数即可obj.fn=fn; obj.fn();

原理如图:对象.属性成员访问时,this指向对象本身
JS面向对象:this全解_第1张图片

Function.prototype.call = function call(context, ...params) {
    context = context == null ? window : context;

    let result;
    // 把函数作为对象的某个成员值
    context["fn"] = this;
    // 基于 对象[成员]() 方式把函数执行,此时函数中的this就是对象,(把参数传递给函数,并且接收返回值)
    result = context["fn"](...params);
    // 设置的成员用完后删掉
    delete context["fn"];
    // 把函数的返回值作为call方法执行的结果返回
    return result;
}
  1. 如果原始对象里有名为fn的属性,就会覆盖掉
    解决办法:通过Symbol创建一个唯一值
  2. context必须是个对象,否则无法设定属性值
    1. 如果不是对象,可以利用其构造函数的constructor转为对应类型,但是注意此方法不适用symbol和bigint类型,他们不允许被new
      JS面向对象:this全解_第2张图片

    2. 也可以通过Object()转为对象

优化

Function.prototype.call = function call(context, ...params) {
    context = context == null ? window : context;
    // 必须要保证CONTEXT得是一个对象
    let contextType = typeof context;
    if (!/^(object|function)$/i.test(contextType)) {
        // context.constructor:当前值所属的类 
        // context = new context.constructor(context);  //=>不适合Symbol/BigInt
        context = Object(context);
    }

    let result;
    let key = Symbol('KEY');
    // 把函数作为对象的某个成员值(成员名唯一:防止修改原始对象的结构值)
    context[key] = this;
    // 基于“对象[成员]()”方式把函数执行,此时函数中的THIS就是对象(把参数传递给函数,并且接收返回值)
    result = context[key](...params);
    // 设置的成员用完后删除掉
    delete context[key];
    // 把函数的返回值作为CALL方法执行的结果返回
    return result;
};

call 的深层理解

var name = 'lc';
function A(x, y) {
    var res = x + y;
    console.log(res, this.name);
}
function B(x, y) {
    var res = x - y;
    console.log(res, this.name);
}
B.call(A, 40, 30);  // 10 "A"
B.call.call.call(A, 20, 10);    // => A.call(20, 10) => NaN undefined
Function.prototype.call(A, 60, 50); // 无任何输出
Function.prototype.call.call.call(A, 80, 70);   // NaN undefined

根据call源码拆分每一步

B.call(A,40,30);	// 10 "A"

JS面向对象:this全解_第3张图片

B.call.call.call(A, 20, 10);	// NaN undefined

JS面向对象:this全解_第4张图片

Function.prototype.call(A, 60, 50);

JS面向对象:this全解_第5张图片

Function.prototype.call.call.call(A, 80, 70);

同第二步

bind

想要改变点击事件的this

function func(x, y, ev) {
    console.log(this, x, y, ev);
}
const obj = {
    name: "test"
};
document.body.onclick = func; //=>this:body  x:MouseEvent  y:undefined
document.body.onclick = func.call(obj, 10, 20); 

这样处理不行,事件绑定,绑定的是一个方法,此处是先把func执行(做了一些处理),把方法执行的返回结果赋值给事件绑定

解决方法:包一层匿名函数先执行call

document.body.onclick = function anonymous(ev) {
    func.call(obj, 10, 20, ev);
};

和直接用bind是一样的效果

document.body.onclick = func.bind(obj, 10, 20);

手写bind

  • 利用科里化函数的编程思想,预先把需要处理的函数/改变的this以及传递的实参等信息存在闭包中,后期满足条件(事件触发/定时器等),先执行返回的匿名函数,在执行匿名函数的过程中再去改变this等
function func(x, y, ev) {
    console.log(this, x, y, ev);
}
const obj = {
    name: "test"
};

Function.prototype.bind = function bind(context, ...params) {
    // this -> 处理的函数  func
    // context -> 要改变的函数中的THIS指向  obj
    // params -> 最后给函数传递的实参  [10,20]
    // 保存func,便于匿名函数中执行,也可以使用箭头函数,就不需要保存this了
    let _this = this;
    // 返回一个匿名函数
    return function /* anonymous */(...args) {
        // args -> 可能传递的事件对象等信息  [MouseEvent]
        // this -> 匿名函数中的THIS是由当初绑定的位置触发决定的(总之不是func要处理的函数),所以需要保存_this
        _this.call(context, ...params.concat(args))
    }
}
document.body.onclick = func.bind(obj, 10, 20)

apply

apply应用:获取数组中的最大值

使用排序

   console.log(arr.sort((a, b) => b - a)[0])

利用Math.max
Math.max 需要一项项传入数值进行比较

    let arr = [4, 6, 10, 3, 5];
 
    console.log(Math.max.apply(Math, arr))

例题

1

var num = 10;
var obj = {
    num: 20
};
obj.fn = (function (num) {
    this.num = num * 3;
    num++;
    return function (n) {
        this.num += n;
        num++;
        console.log(num);
    }
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);

2

以下代码的this是什么

button.onclick = function(e) {
  console.log(this)
}

回答:不知道
因为this是call的第一个参数,这里并未执行这个事件,所以未传入this
标准回答:

  1. 这个this是不确定的,需要看它如何调用

  2. 如果点击的是button的时候,浏览器会将button作为this传进来

  3. 如果通过其它方式调用就要看传入的this是什么了

    let button = document.createElement('button');
    button.innerHTML = "button";
    document.querySelector('body').appendChild(button);
    button.onclick = function(e) {	// 点击指向button
      console.log(this)
    }
    
    let fn = button.onclick;
    fn();	// 不传this自调用默认指向window
    fn.call({name: "LC"})	// 自调用传入指定this
    

    在这里插入图片描述

3

注意点

  1. let声明的变量不会挂在window
  2. 判断this时可以用call来尝试改写调用函数
let length = 10
function fn(){console.log(this.length)}

let obj = {
  length: 5,
  method(fn){
    fn()
    arguments[0]() 
  }
} 
obj.method(fn, 1)

在这里插入图片描述

调用的第一个fn()的this就是指向windowwindow.length

let obj = {
  length: 5,
  method(fn){
    fn.call(window)
  }
} 

第二个调用中this指向的是arguments,输出arguments的长度

arguments[0].call(arguments) 
 let length = 10
function fn(){console.log(this.length)}

let obj = {
  length: 5,
  method(fn){
    arguments[0].call(arguments) // 4
  }
} 
obj.method(fn, 1, 2, 3)

你可能感兴趣的:(前端技术笔记)