关于 this
承接上一篇文章JS中几种最基本的this情况分析,加入面向对象的情况,总结一下
关于 this 的几种情况
- 给当前元素的某个事件行为绑定方法,方法中的
this是当前元素本身(排除:DOM2在IE6~8中基于attachEvent进行事件绑定,这样处理方法中的this->window) - 方法执行,看方法前面是否有“点”,有“点”,“点”前面是谁
this就是谁,没有“点”,this就是window(严格模式下是undefiend)- 自执行函数中的
this一般是window/undefined - 回调函数中的
this一般是window/undefined(当然某些方法中会做一些特殊的处理) - 括号表达式有特殊性
- ......
- 自执行函数中的
new执行方式构造函数,构造函数体中的this是当前类的实例- 箭头函数中没有
this(类似的还有块级上下文),所以无论怎样去执行,怎样去修改,都没有用,如果函数中出现this,一定是其所在的上级上下文中的this - 在
Function.prototype提供了三个方法:call/apply/bind,这三个方法都是用来强制改变函数中this指向的(每一个函数都是Function的实例,所以都可以调取这三个方法执行)
'use strict'
let obj = {
name: 'ttt',
fn() {
let self = this;
setTimeout(function () {
console.log(this);// this -> window
console.log(self);//self->obj
}, 1000);
setTimeout(() => {
console.log(this);
// this是上级上下文中的,也就是obj
}, 1000);
}
}
obj.fn();
call / apply 的使用和手写
call
想在fn执行的时候将其中的 this 转化为 obj ,并且传入其他参数
"use strict";
const fn = function fn(x,y){
console.log(this.name,x+y);
};
let obj = {
name: 'obj'
};
fn.call(obj,10,20)//'obj' 30
fn.call(obj,10,20) 运行如下:
fn 首先作为 Function 的实例,基于 __proto__ 找到 Function.prototype.call 方法,并且把找到的 call 方法执行
call 方法执行时的一些步骤:
call中的this->fn- 第一个参数
context->obj,是未来要改变的call函数中的this指向,让其指向第一个参数context - 剩余的参数
params->[10,20]存储的是未来要给函数传递的实参
call 方法执行的作用:帮助我们把 fn ( call 中的 this )执行,并且让方法中的 this 指向 obj ( context ),顺便把 10/20 ( params )传递给函数
注意点:
-
如果直接执行
fn.call()一个值都不传的话,fn中的this非严格模式下是window,严格模式下是undefined。x为undefined,y为undefined -
如果执行
fn.call(null/undefined),非严格模式下this是window,严格模式下是null/undefined(即传什么就是什么) 。x为undefined,y为undefined
总结:即严格模式下传入什么 this 就是什么,非严格模式下,传入 null 或者 undefined this 是 window
例如执行 fn.call(10, 20) : this -> 10 , x -> 20 , y -> undefined
apply
apply 和 call 只有一个区别: call 方法在设定给函数传递的实参信息的时候,是要求一个个传递实参值。但是 apply 是要求用户把所有需要传递的实参信息以数组/类数组进行管理的。虽然要求以数组方式传进去,但是内部最后处理的时候,和 call 一样,也是一项项的传递给函数的。
fn.apply(obj, [10, 20])
所以一个函数创建出来的时候,其中的 this 直到执行的时候才可以确定。执行时, this 在不同的环境可以时自动关联的值,也可以是我们使用 call / apply / bind 手动关联的值。
应用场景
场景一:求最大值或最小值
求一个数组中的最大值或者最小值
let arr = [10, 14, 23, 34, 20, 13];
-
排序处理 时间复杂度稍微高一些(
sort内部处理的时间复杂度是 N^2,即遍历两次)console.log(arr.sort((a, b) => b - a)[0]) -
假设法 时间复杂度N,循环一次即可
let max = arr[0], i = 1, len = arr.length, item; for (; i < len; i++) { item = arr[i]; if (item > max) { max = item; } } console.log(max); -
Math.max获取最大值
console.log(Math.max(10, 14, 23, 34, 20, 13)); //=>34
console.log(Math.max(arr)); =>NaN
方法本身是传入的所有参数中的最大值,需要把比较的数字一项项的传递给 max 方法才可以
那么把数组中的每一项分别传递给 max 方法:
let max = Math.max(...arr);
console.log(max); =>34
let max = Math.max.apply(null, arr);
console.log(max);// =>34
利用 apply 的机制。虽然传递给 apply 的是一个数组,但是 apply 内部会把数组中的每一项分别传递给对应的函数。而且 Math.max 中用不到 this ,所以 this 改成谁无所谓,就是占个位而已
场景二:任意数求和
任意数求和(不确定实参的个数,所以无法基于设定形参接受) 两种方法:
- 剩余运算符 ES6
argumentsES3
-
const sum = function sum(...params) { if (params.length === 0) return 0; //params -> 数组 return params.reduce((total, item) => total + item, 0); }; -
把类数组转换为数组
params = Array.from(params);ES6+params = [...params];ES6+- ...
那么如何使用es5以下方法将伪数组转换为数组?
最简单的方法:手动一个个转换
const sum = function sum() { let params = arguments; // params -> 类数组「不能直接使用数组的办法」 if (params.length === 0) return 0; let i = 0, len = params.length, arr = []; for (; i < len; i++) { arr[arr.length] = params[i]; <==> arr.push(params[i]); } return arr.reduce((total, item) => total + item, 0); };借助
slice:ary.slice() ary.slice(0)以上都为数组的克隆,把原始数组中的每一项都克隆一份给返回的新数组(浅克隆)
解释一下为什么
[].slice.call(arguments)会将伪数组集合变为真的数组-
查看polyfill,
slice原理是用了for...i循环+往真是数组里循环存值,所以[].slice.call(arguments)会把polyfill代码中的this替换为arguments,并且循环,然后往真的数组里按序存值,最后返回真的数组 -
因为类数组的结果和数组非常的相似(都有下标+
length),所以大部分操作数组的代码,也同样适用于类数组,在这个前提下,我们只需要把实现好的数组方法执行,让方法中的this变为类似组,就相当于类数组在操作这写代码,实现了类数组借用数组方法的目的,我们这种操作叫做“鸭子类型”
模拟
slice原理:Array.prototype._slice = function slice() { //模拟 //this -> ary let i = 0, len = this.length, arr = []; for (; i < len; i++) { arr[arr.length] = this[i]; } return arr; };把类数组转换为数组:如果我们能把
slice执行,并且让slice中的this是arguments,这样就相当于在迭代arguments中的每一项,把每一项赋值给新的数组集合 -> 也就是把类数组转换为数组- 如何让
slice执行 : 使用Array.prototype.slice()/[].slice()..... - 如何改变
slice中的this: 使用call/apply->params = [].slice.call(params);
const sum = function sum() { let params = arguments; if (params.length === 0) return 0; params = [].slice.call(params); return params.reduce((total, item) => total + item, 0); };或者不转换了,直接借用这种规则即可
const sum = function sum() { let params = arguments; if (params.length === 0) return 0; //不转换了,直接借用即可 return [].reduce.call(params, (total, item) => total + item, 0); };或者直接修改原型链指向,这样调用的时候,里面的
this就指向了argumentsconst sum = function sum() { let params = arguments; //params.__proto__ = Array.prototype; 修改原型链的指向 arguments可以直接使用数组原型上的任何方法 //或者: params.reduce = Array.prototype.reduce; if (params.length === 0) return 0; return params.reduce((total, item) => total + item, 0); };
一个有趣的问题:
let obj = {
2: 3, //1
3: 4, //2
length: 2, //从2->3,从3->4
push: Array.prototype.push
};
obj.push(1)
obj.push(2)
console.log(obj[2])//1
console.log(obj[3])//2
console.log(obj.length)//4
原理是 push 中的 this 变为了 obj
那么push做了什么事?
手动模拟一下 push (push是内置的):
Array.prototype.push = function push(val) {
//这些代码只是一些模拟
// this -> 操作的数组
this[this.length]=val;
this.length++;
};
arr.push(100);
把 push 方法执行,让里面的 this -> obj 类似于
obj.push(1) => [].push.call(obj,1) :
obj[obj.length]=1=>obj[2]=1;obj.length++;
obj.push(2) :
obj[obj.length]=2=>obj[3]=2obj.length++
手写 call / apply
用js模拟 call
面试题:完成change函数
~ function () {
/* 内置CALL实现原理 */
function change(context, ...params) {
};
Function.prototype.change = change;
}();
let obj = {
name: 'Alibaba'
};
function func(x, y) {
this.total = x + y;
return this;
}
let res = func.change(obj, 100, 200);
console.log(res);
//res => {name:'Alibaba',total:300}
实际上就是内置 call 的实现原理
call 的核心原理很简单:
obj.func = func
console.log(obj.func(100,200))
delete obj.func
拿上面例子举例就是让 obj 中有一个 func 属性,然后执行 obj.func ,那么其中的 this 就自动指向了 obj ,最后再删除,打印如下( console.log 打印的是最后运行代码,即删除完 func 的对象)
~ function () {
//在没有Symbol之前,我们可以用一个函数随机生成随机值来代替 Symbol('KEY')
/*const createRandom = () => {
let ran = Math.random() * new Date();
return ran.toString(16).replace('.', '');
};*/
/* 内置CALL实现原理 */
function change(context, ...params) {
// this->要执行的函数func context->要改变的函数中的this指向(当下例子中为obj)
// params->未来要传递给函数func的实参信息{数组} [100,200]
// 临时设置的属性,不能和原始对象冲突,所以我们属性采用唯一值处理
context == null ? context = window : null;
if (!/^(object|function)$/.test(typeof context)) context = Object(context);
let self = this,
key = Symbol('KEY'),
result;
context[key] = self;
result = context[key](...params);
delete context[key];
return result;
};
Function.prototype.change = change;
}();
change函数中
this->要执行的函数funccontext->要改变的函数中的this指向(当下例子中为obj)params->未来要传递给函数func的实参信息{数组}[100,200]
要注意的是:
context的要执行的那个属性是临时设置的属性,不能和原始对象冲突,所以我们属性采用唯一值Symbol处理。- 传进来的
context不一定是对象,只有对象设置属性才有用所以添加判断,如果传进来的值不是objec类型的,就要转换为对象if (!/^(object|function)$/.test(typeof context)) context = Object(context) - 当传递的是
null或者undefined的时候,都应该将this指向window所以 :context == null ? context = window : null;
在没有 Symbol 之前,我们可以用一个函数随机生成 context 的 key
模拟 apply
apply和call唯一的区别就是,apply传入得失参数数组,
所以将上面calll实现的方法去掉 ... 不使用扩展运算符即可
function call(context, ...params) {}
function apply(context, params) {}
伪数组转化为真数组方法
小tips:将获取的参数转化为真正数组的方法:
function toArray(...params) {
return params;
}
function toArray() {
// arguments 类数组集合
// return [...arguments];
// return Array.from(arguments);
return [].slice.call(arguments);
}
bind 的使用与手写
bind的使用
const a1 = 'a1'
const a2 = 'a2'
const a3 = 'a3'
function fn(p1,p2){
console.log('this',this)
console.log(arguments)
console.log('p1',p1)
console.log('p2',p2)
}
const fn2 = fn.bind('mtt',a1,a2,a3)
fn2(3,4,5)
输出结果:
其实上面的bind就是函数柯里化的应用,预先传入一些参数,先处理,然后返回函数。
举例子:
要求:
~function () {
//bind方法在ie6-8中不兼容,接下来我们自己基于原生js实现这个方法
function bind(context, ...params) {
}
Function.prototype.bind = bind;
}();
var obj = {name: 'ali'}
function func() {
console.log(this, arguments)
//当点击body 的时候,执行function方法,并且输出obj [100,200,MouseEvent事件对象]
}
document.body.onclick = func.bind(obj, 100, 200)
分析:
bind & call / apply 区别:
- 都是为了改变函数中的
this指向 call/apply:立即把函数执行bind:不是立即把函数执行,只是预先把this和后期需要传递的参数存储起来(预处理思想 -> 柯理化函数)
bind 的原理:其实就是利用闭包的机制,把要执行的函数外面包裹一层函数,预先把 this 和后期需要传递的参数存储起来。即柯理化思想的运用
事件相关分析:
function func(ev){
console.log(this,arguments)
}
document.body.onclick = func
浏览器在点击事件触发时,会自动绑定 this 为事件触发的DOM对象(这里是 body )并且会默认传入事件对象作为参数
假设我们用内置的 bind ,看看结果如何:
var obj = {name: 'ali'}
function func() {
console.log(this, arguments)
}
document.body.onclick = func.bind(obj, 100, 200)
点击之后:
不仅把原来的参数穿进去,并且最后在末尾,还有传入的事件对象
题目最后目的:把 func 执行, this 改为 obj ,参数 100/200/ev 传递给他即可,如下:
document.body.onclick = function proxy(ev) {
// 最后的目的:把func执行,this改为obj,参数100/200/ev传递给他即可
func.call(obj, 100, 200, ev);
};
手写 bind
了解以上信息之后开始手写:
~ function () {
/* 内置BIND的实现原理 */
function bind(context, ...params) {
// this->func context->obj params->[100,200]
let self = this;
return function proxy(...args) {
// args->事件触发传递的信息,例如:[ev]
params = params.concat(args);//把两次传入的参数进行拼接
return self.call(context, ...params);//改变this指向
};
};
Function.prototype.bind = bind;
}();