课时1 :JS中关于THIS的五种情况分析
(一)this简介
1.this的分布
THIS:全局上下文中的THIS是WINDOW;块级上下文中没有自己的THIS,它的THIS是继承所在上下文中的THIS的;在函数的私有上下文中,THIS的情况会多种多样,也是接下来我们重点研究的;
①块级中的this
{
let a = 12;
console.log(this)//=>window
}
------------------------------
let obj = {
fn(){
{
let a = 12;
console.log(this)//=>obj
}
}
};
obj.fn()
2.执行主体和执行上下文
THIS不是执行上下文(EC才是执行上下文),THIS是执行主体
例如:张三拿着加了五个鸡蛋的鸡蛋灌饼去北京大饭店吃早餐(事情本身是吃早餐,张三吃早餐,这件事情的主体是奂康【THIS】,在北京饭店吃,北京饭店是事情发生所在的上下文【EC】)。
3.如何区分执行主体:
1. 事件绑定:给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的THIS是当前元素本身(特殊:IE6~8中基于attachEvent方法实现的DOM2事件绑定,事件触发,方法中的THIS是WINDOW而不是元素本身)
/*html*/
*{
margin:0;
padding:0
}
html,
body{
height:100%;
background:lightblue;
}
/*事件绑定 DOM0*/
let body = document.body;
body.onclick = function () {
// 事件触发,前提是点击并方法执行,方法中的THIS是BODY(当前元素本身);方法若(函数)不执行,函数只是一个堆,堆里面都是代码字符串,只有方法执行才能形成私有上下文,在整个过程中才会初始化this,this指向谁谁,方法不执行谁都不知道this是谁?
console.log(this);//body
};
/*DOM2*/
body.addEventListener('click', function () {
console.log(this); //=>BODY
});
IE6~8中的DOM2事件绑定
box.attachEvent('onclick', function () {
console.log(this); //=>WINDOW
});
2. 普通方法执行(包含自执行函数执行、普通函数执行、对象成员访问调取方法执行等):只需要看函数执行的时候,方法名前面是否有“点”,有“点”,“点”前面是谁THIS就是谁,没有“点”THIS就是WINDOW[非严格模式]/UNDEFINED[严格模式]。
/*匿名函数中的this*/
(function () {
console.log(this); //=>window
})();
let obj = {
fn: (function () {
console.log(this); //=>window 函数中的this是谁和在哪执行以及在哪定义的 没有关系
return function () {}
})() //把自执行函数执行的返回值赋值给OBJ.FN
};
/*普通函数执行与对象成员访问调取方法执行*/
function func() {
console.log(this);
}
let obj = {
func: func
};
func(); //=>方法中的THIS:WINDOW
obj.func(); //=>方法中的THIS:OBJ
/*slice()与Array.prototype与[].__proto__*/
[].slice(); //=>数组实例基于原型链机制,找到ARRAY原型上的SLICE方法([].slice),然后再把SLICE方法执行,此时SLICE方法中的THIS是当前的空数组
Array.prototype.slice(); //=>SLICE方法执行中的THIS:Array.prototype
[].__proto__.slice(); //=>SLICE方法执行中的THIS:[].__proto__===Array.prototype
function func() {
// THIS => WINDOW
console.log(this);//函数中的this是谁和在哪执行以及在哪定义的 没有关系
}
document.body.onclick = function () {
// THIS => BODY
func();
};
3. 构造函数执行(NEW XXX):构造函数体中的THIS是当前类的实例
function Func() {
this.name = "F";
console.log(this); //=>构造函数体中的THIS在“构造函数执行”的模式下,是当前类的一个实例,并且THIS.XXX=XXX是给当前实例设置的私有属性
}
let f = new Func;
Func.prototype.getNum = function getNum() {
// 而原型上的方法中的THIS不一定都是实例,主要看执行的时候,“点”前面的内容
console.log(this);
};
f.getNum();
f.__proto__.getNum();
Func.prototype.getNum();
4.ES6中提供了ARROW FUNCTION(箭头函数): 箭头函数没有自己的THIS,它的THIS是继承所在上下文中的THIS。和块级上下文很相似。箭头函数中没有自己的执行主体。
【普通函数执行】:形成私有上下文(和AO),初始化作用域链,初始化THIS,初始化ARGUMENTS,形参赋值,变量提升,代码执行。
【箭头函数执行】:形成私有上下文(和AO),初始化作用域链,形参赋值,变量提升,代码执行,代码执行的时候遇到THIS直接找上级上下文中的THIS。
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
不建议乱用箭头函数(部分需求用箭头函数还是很方法便的)
【回调函数】:把一个函数当成值传给另一个函数,比如arr.forEach(function(){})和下文里定时器里面的匿名函数。
let obj = {
i: 0,
// func:function(){}
func() {
// THIS:OBJ
setTimeout(function () {
// THIS:WINDOW 回调函数中的THIS一般都是WINDOW(但是有特殊情况)
this.i++;
console.log(this);//0
}, 1000);
}
};
obj.func();
但是我们的需求是obj的i变成1.
/*很早以前的方案*/
let obj = {
i: 0,
// func:function(){}
func() {
// THIS:OBJ
let _this = this;
setTimeout(function () {
// THIS:WINDOW 回调函数中的THIS一般都是WINDOW(但是有特殊情况)
_this.i++;
console.log(_this);//1
}, 1000);
}
};
obj.func();
/*基于BIND强制改变this指向*/
let obj = {
i: 0,
func() {
setTimeout(function () {
// 基于BIND把函数中的THIS预先处理为OBJ
this.i++;
console.log(this);
}.bind(this), 1000);
}
};
obj.func();
/*回调函数*/
let obj = {
i: 0,
func() {
setTimeout(() => {
// 箭头函数中没有自己的THIS,用的THIS是上下文中的THIS,也就是OBJ
this.i++;
console.log(this);
}, 1000);
}
};
obj.func();
5. 可以基于CALL/APPLY/BIND等方式,强制手动改变函数中的THIS指向:这三种模式是和直接很暴力的(前三种情况在使用这三个方法的情况后,都以手动改变的为主)
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);
每一个函数都是对象,天生自带一些东西。prototype:对象。Funtion.protype:__proto每一个函数都是大写Function这个类的实例,所以它的原型链指向Function原型.
理解:把当前自执行函数执行的返回结果给了obj下的fn,obj.fn就等于当前自执行函数执行返回的结果小函数,当前函数执行,形成上下文,上下文中的某个东西BBBFFF000被上下文以外中obj.fn所占用了,所以当前上下文不能释放。所以当前上下文更不能被释放了,因为之前只有obj.fn用到它了,现在不仅obj.fn用到它了,而且全局下的fn也用到它了,所以更不能被释放了。
课时2 CALL和APPLY以及BIND语法(BIND的核心原理)
1.共同点
这三个方法都是用来改变函数中的THIS的
2.Function.prototype:
call:[function].call([context],params1,params2,…) [function]作为Function内置类的一个实例,可以基于__ proto _ _找到Function.prototype的call方法,并且把找到的call方法执行;在call方法执行的时候,call内y部会把[function]执行,并且把函数中的THIS指向为[context],并且把params1,params2…等参数值分别传递给函数。和[].slice一样。
apply:[function].apply([context],[params1,params2,…]) 和call作用一样,只不过传递给函数的参数需要一数组的形式传递给apply**。call()和apply()唯一的区别在于传参的形式不一样**。
bind:[function].bind([context],params1,params2,…) 语法上和call类似,但是作用和call/apply都不太一样;call/apply都是把当前函数立即执行,并且改变函数中的this指向的,而bind是一个预处理的思想,基于bind只是预先把函数中的this指向[context],把params这些参数值预先存储起来,但是此时函数并没有被执行。
3.call和apply
let obj = {
name:"obj"
};
function func(x,y){
console.log(this,x,y)
}
func(10,20); //=>THIS:WINDOW
obj.func(); //=>Uncaught TypeError: obj.func is not a function
(1)func.call(obj,10,20);//{name:"obj"} 10 20
(2)func.apply(obj,[10,20]);//{name:"obj"} 10 20
①需求实现:func()执行并让this指向obj。
func.call():让当前函数通过原型链找到Function原型上的call方法,因为当前函数肯定是Function类的一个实例,当我把Function原型上的call方法执行的时候,可以让func执行,call方法执行首先让func执行,并且让func中的this变成第一个参数,并且你想给func传参,需要把这些参数先一项一项先传给call,call把这些参数传给func。
func.apply():虽然传递给apply的参数值都是以数组形式传递的,但apply内部会帮我把数组中的每一项传给func。
②call方法的第一个参数,如果不传递或者传递的是null/undefiend,在非严格模式下都是让this指向window(严格模式下传递的是谁,this就是谁,不传递this是undefined)。
func.call(); //window //undefined
func.call(null); //window "use strict" //null
func.call(undefined);//window //undefined
func.call(11); //Number{11} //11
4.bind
let body = document.body;
let obj = {
name: "obj"
};
function func(x, y) {
console.log(this, x, y);
}
【func与func(10, 20)】
body.onclick = func; //=>把func函数本身绑定给body的click事件行为,此时func并没有执行,只有触发body的click事件,我们的方法才会执行
body.onclick = func(10, 20); //=>先把func执行,把方法执行的返回结果作为值绑定给body的click事件
【需求】:把func函数绑定给body的click事件,要求当触发body的点击行为后,执行func,但是此时需要让func中的this变为obj,并且给func传递10,20
①body.onclick = func.call(obj, 10, 20); //=>这样不行,因为还没点击func就已经执行了,call是立即执行
②body.onclick = func.bind(obj, 10, 20);//预先让func中要改变this的obj先存储起来,最后把10和20传给func,当触发body点击事件行为的时候会把func执行,把预先处理的一些值让func的this变成obj,让10和20传给func。
③在没有bind的情况下我们可以这样处理(bind不兼容IE6~8)【先绑定匿名函数,再使用func.call】
body.onclick = function anonymous() {
func.call(obj, 10, 20);
};
/*bind方法实现*/
//执行BIND(BIND中的THIS是要操作的函数),返回一个匿名函数给事件绑定或者其它的内容,当事件触发的时候,首先执行的是匿名函数(此时匿名函数中的THIS和BIND中的THIS是没有关系的)
// BIND的内部机制就是利用闭包(柯理化函数编程思想)预先把需要执行的函数以及改变的THIS再以及后续需要给函数传递的参数信息等都保存到不释放的上下文中,后续使用的时候直接拿来用,这就是经典的预先存储的思想
Function.prototype.bind = function bind(context = window, ...params) {
//this->func
let _this = this;
return function anonymous(...inners) {
//this与执行主体有关,此处默认指向事件body,如果是定时器中的匿名函数,默认指向window
// _this.call(context, ...params);
_this.apply(context, params.concat(inners));
};
};
(1)body.onclick = func.bind(obj, 10, 20);
// body.onclick = function anonymous(ev) { //=>ev事件对象
// func.call(obj,10,20,ev);
//};
(2)setTimeout(func.bind(obj), 1000);
// setTimeout(function anonymous() {
//
// }, 1000);
bind整个特点:bind执行形成的私有上下文,在当前私有上下文中,返回一个小函数或堆【return function anonymous(…inners) {】,我把返回来的堆赋给当前上下文以外的其他东西【body.onclick = func.bind(obj, 10, 20);】(事件绑定,定时器…),当前上下文是不能销毁的,形成了闭包。bind的核心思想就是利用了堆栈内存中的闭包概念,当bind形成一个不被销毁的上下文,那么第一次传给bind的参数【function bind(context = window, …params) {//this->funclet _this = this;】就保留在当前这个不销毁的上下文中了。当我们把返回来的匿名函数给这个事件绑定,当触发事件绑定,把方法执行,执行匿名函数,在这个小函数中用到我事先存储起来的【function bind(context = window, …params) {//this->funclet _this = this;】所谓的函数,this指向,参数信息。所以我们可以理解为每一个func.bind执行的时候,利用bind这种闭包的机制预先把我们以后要执行的函数,预先把我们函数要改变的this,预先把我们未来要给函数传的参数全部存储到闭包当中(不销毁的上下文中)未来如果当我们事件真正触发执行这个函数的时候,我们需要拿这些东西的时候,直接拿来用就好了。这就是利用闭包机制,预先形成一个不销毁的上下文,在里面存了我们未来想要做的事情,未来想要用的时候,直接拿来用。这就是JS中闭包的高阶应用—柯里化函数编程思想,bind就是最经典的函数柯里化思想。