一、事件綁定
THIS1: 給元素的某個事件行爲綁定方法,事件觸發,方法執行,此時方法中的this一般都是當前元素本身
在代碼中,有一個button
<button id="btn">點我一下~</button>
<script>
//DOM0事件綁定
// btn.onclick = function anonymous() {
// console.log(this)
// }
// DOM2事件綁定
// btn.addEventListener('click', function anonymous() {
// console.log(this); //元素
// }, false) //在冒泡階段執行,不兼容IE678
// btn.attachEvent('onclick',function anonymous(){
// // <= IE8瀏覽器中的DOM2事件綁定
// console.log(this); //window
// })
// function fn() {
// console.log(this);
// }
// btn.onclick = fn.bind(window); //=>fn.bind(window):首先會返回一個匿名函數(AM),把AM綁定給事件;點擊觸發執行AM,AM中的THIS是元素,但是會在AM中執行FN,FN中THIS是預先指定的WINDOW
/*
bind原理:
(function(){
return function(){ //執行的時候是執行這個函數
fn.call(window);
}
})()
*/
</script>
二、普通函數執行
THIS2:普通函數執行,它裏面的THIS是誰,取決於方法執行前面是否有“.”點,有的話,“點”前面是誰THIS就是誰,沒有THIS指向WINDOW(嚴格模式下是UNDEFINED)
<script>
function fn() {
console.log(this);
}
let obj = {
name: 'OBJ',
fn: fn
};
fn();
obj.fn();
console.log(obj.hasOwnProperty('name'));//=>hasOwnProperty方法中的this:obj 輸出爲TRUE
console.log(obj.__proto__.hasOwnProperty('name')); //=>hasOwnProperty方法中this:obj.__proto__(即Object.prototype原型) 輸出爲FALSE
console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); //=>讓object原型的hasOwnproperty方法的this強制變成obj <==> 等價於obj.hasOwnProperty('name'),
/*
hasOwnProperty用來檢測某個屬性名是否屬於當前對象的私有屬性。
in用來檢測是否爲其屬性(不論私有還是共有)
*/
console.log(obj.hasOwnProperty('name')); //=>true
console.log(obj.hasOwnProperty('toString'));//=>false
console.log('toString' in obj); //=>true
</script>
三、構造函數執行
THIS3:構造函數執行(new xxx),函數中的this是當前類的實例
<script>
function Fn() {
console.log(this); //Fn {}
// => this.xxx = xxx 是給當前實例設置私有屬性
}
let f = new Fn;
</script>
四、箭頭函數
THIS4:箭頭函數中的沒有自身的THIS,所用到的THIS就是其上下文中的THIS
<script>
/*
箭頭函數沒有的東西很多:
1.它沒有prototype(也就是沒有構造器),所以不能被new執行
2.它沒有arguments實參集合(可以基於...args剩餘運算符獲取)
箭頭函數如下所示:
let obj = {
fn: () => {
// this:Window
console.log(this);
}
};
obj.fn();
*/
// 舉個小栗子
// let obj = {
// name: 'OBJ',
// fn: function () {
// console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
// return function () {
// console.log(this);
// }
// }
// };
// let ff = obj.fn(); //將obj.fn()執行,後將值賦值給ff
// ff(); //就是上述的ff函數,方法執行,沒有".",所以this是WINDOW 輸出window
/*新的小需求:
若想在以上小例子中,obj.fn()中返回的函數,也就是ff()方法中,修改obj.name的值,就要指定this是obj,現在小函數的this指向window,若直接這麼寫:
let obj = {
name: 'OBJ',
fn: function () {
console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
return function () {
console.log(this);
this.name = '你好世界'; //修改的是window.name,不符合需求
}
}
};//修改的是window.name,不符合需求
*/
// 故有下面兩種方法修改:
// // 寫法一:
// let obj = {
// name: 'OBJ',
// fn: function () {
// console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
// let _this = this; //將外面和返回函數中的this都指向obj
// return function () {
// _this.name = '你好世界'//這裏就修改成功了,返回的這個函數在此刻已經將obj.name='OBJ',改爲你好世界,可以直接在控制檯輸入obj,就可以看到結果
// //console.log(this);
// }
// }
// };
// let ff = obj.fn();
// ff();
// // 寫法二:
// let obj = {
// name: 'OBJ',
// fn: function () {
// // console.log(this); //this:obj,輸出爲{name: "OBJ", fn: ƒ}
// return () => { //箭頭函數,使用的是上下文中的this,因爲上面的this是obj,所以直接這麼寫就修改成功了
// console.log(this); //=>{name: "OBJ", fn: ƒ} this:obj
// this.name = '你好世界';
// console.log(this); //this:obj => 控制檯輸出{name: "你好世界", fn: ƒ},已經修改成功
// }
// }
// };
// let ff = obj.fn();
// ff();
// 若想要使用setTimeout,1秒後修改obj.name,可以直接使用箭頭函數
let obj = {
name: 'OBJ',
fn: function () {
/*
若直接這麼寫還是會修改window.name
setTimeout(function(){
this.name = '你好世界';
console.log(this);
},1000);
*/
setTimeout(_ => {
this.name = '你好世界';
console.log(this); //{name: "你好世界", fn: ƒ}
}, 1000);
}
};
let ff = obj.fn();
</script>
五、call/apply/bind
<script>
/*
THIS5:基於call/apply/bind可以改變函數中this的指向(強行改變)
CALL/APPLY:
第一個參數就是改變的this指向,寫誰就是誰(特殊:非嚴格模式下,傳遞null/undefined指向的也是window):func.call([context])與func.apply([context])
兩者唯一區別:執行函數,傳遞的參數方式有區別,call是一個個傳遞,apply是把需要傳遞的參數放到數組中整體傳遞
若想傳兩個值10和20:call傳遞方法:func.call([context],10,20);
apply傳遞方法:func.apply([context],[10,20]);數組形式傳遞
BIND:
call和apply都是改變this的同時直接把函數執行了,而bind不是立即執行函數,屬於預先改變this和傳遞一些內容 => "柯理化函數編程思想"
*/
//Function.prototype = {call/apply/bind...}function的原型上的方法
// 若自己寫一個bind/call/apply方法源碼
~ function anonymous(proto) {
// 重置內置的bind--ES5的寫法,具體思路如下:
/*
function bind(context) {
// context 可能是null || undefined
if (context == undefined) {
context = window;
}
// 獲取傳遞的數組集合
// =>傳遞的值可能是類數組,arguments:{0:context,1:10,2:20,length:3}
// =>想要獲取除了第一個的值(因爲第一個參數是要綁定的對象context),可以使用slice方法,但是這個arguments不是數組,它的原型鏈指向Object.prototype,
// => console.log(arguments); ...__proto__:... constructor: ƒ Object()指向Object
// 想使用slice,若是數組的話,直接slice(1),從索引1開始,直接取到最後,是它的值。但此時的arguments不是數組,就不能直接調用這個方法,slice不是Object原型上的方法,是屬於數組的原型上的方法,可以借用數組的這個方法
// =>像上文中提及的方法一樣,obj.hasOwnproperty('name')與直接調用Object的原型上的方法,將this變成obj:Object.prototype.hasOwnProperty.call(obj,'name'),是相同的
// => 所以[].slice.call(arguments,1);就是將this變成arguments,然後去執行數組的slice方法,然後返回結果一定是一個數組。
// args是執行bind時傳遞的參數
var args = [].slice.call(arguments, 1);
// 需要最終執行的函數
var _this = this;
//返回的函數根據具體情況,會有不同:
// 1、 沒有事件行爲時會返回一個新的函數,這麼寫:
// return function anonymous() {
// 改變this爲傳遞的context,且傳遞的參數是數組args
// _this.apply(context, args);
// }
// 2、若綁定的事件方法,會有一個ev的傳參,返回的函數就是:
// return function anonymous(ev) {
// args.push(ev);
// _this.apply(context,args)}
//3、(最終版)若不是事件點擊,可能參數不是ev(不僅是ev,可能還有其他),具體是什麼也不知道,所以就不能像上面註釋中的那麼寫,應該寫成如下形式: =>完整版
return function anonymous() {
//amArgs是將bind給事件或者別的時,執行anonymous時的傳值,將傳進來的參數,借用數組的slice方法變成數組,從索引0開始到最後,變成一個數組。
var amArgs = [].slice.call(arguments, 0);
// 然後將args和傳的參數數組合並
// args = args.concat(amArgs);
// 最後將參數統一給要執行的obj.fn(在此文中的使用場景),在別處是別的要執行的函數
// _this.apply(context, args);
// 也可以將上兩步合起來,直接這麼寫
_this.apply(context, args.concat(amArgs));
}
}
*/
// 重置內置的bind--ES6寫法 重置bind方法
// 經過測試,apply的性能不如call
function bind(context = window, ...args) {
return (amArgs) => this.call(context, ...args.concat(amArgs));
}
/*
// call 重寫改變this方法,在此保證context是引用類型值
function call(context = window, ...args) {
// => 必須保證context是引用類型
// =>this:需要執行的函數
// 因爲call是立即執行的方法,還要傳值,所以是直接執行this(...args);=>但要修改this,可以使用一個方法,有個方法是fn,有個this是obj,想要fn執行時,this是obj,可以直接這麼寫:obj.fn,在此也可以使用這個方法,context是想要改變this指向的對象(就相當於obj),$fn是想要加的一個屬性,等於要執行的那個函數this
context.$fn = this;
// 執行這個函數。並傳參數context.$fn(...args),接收返回結果
let result = context.$fn(...args); //此時要執行的這個函數中,this是context
// 加上這個參數,用過後要刪除
delete context.$fn;
// 最後將執行結果返回
return result;
}
*/
// 重寫call方法,將context類型值全部考慮在內的全面寫法
function call(context = window, ...args) { //這裏識別undefined和具體的context值,識別不了null
// 如果context,要改變的this指向對象===null,讓它===window,否則什麼都不做
context === null ? context = window : null;
// 判斷context,三個條件都不成立,就不是引用類型的值,是基本數據類型
let type = typeof context;
if (type !== 'object' && type !== 'function' && type !== 'symbol') {
// console.log('基本類型');
// => 基本類型值
switch (type) {
case 'number':
context = new Number(context); //可以把context變成數字的引用類型值,構造函數創建的結果都是引用類型的
break;
case 'string':
context = new String(context); //構造函數創建的結果都是引用類型的
break;
case 'boolean':
context = new Boolean(context);//構造函數創建的結果都是引用類型的
break;
}
}
// 在此的context的類型就是引用類型值
context.$fn = this;
// 執行這個函數。並傳參數context.$fn(...args),接收返回結果
let result = context.$fn(...args); //此時要執行的這個函數中,this是context
// 加上這個參數,用過後要刪除
delete context.$fn;
// 最後將執行結果返回
return result;
}
// 重寫apply方法(簡單):context是引用類型值,若想判斷其他類型,同上call中的類型判斷
//與call的區別在於參數,只有兩個,第二個參數是數組
function apply(context = window, args) {
context.$fn = this;
// 執行這個函數。並傳參數context.$fn(...args),接收返回結果,把參數展開
let result = context.$fn(...args); //此時要執行的這個函數中,this是context
// 加上這個參數,用過後要刪除,但是在此,不生效,刪除不了,可不寫
delete context.$fn;
// 最後將執行結果返回
return result;
}
//原型上掛載
proto.bind = bind;
proto.call = call;
proto.apply = apply;
}(Function.prototype)
let obj = {
fn(x, y) {
console.log(this, x, y);
}
// 在anonymous中有ev,在此用的時候也會有ev,若有其他操作,也會顯示
// fn(x, y, ev) {
// console.log(this, x, y, ev);
// }
}
obj.fn.call('1', 10, 20);//這個obj.fn函數執行,要把返回結果接收
obj.fn.apply(window, [10, 20]); //window 10 20
// 若想1秒鐘後執行obj.fn,傳值,寫法爲:
//直接這麼寫setTimeout(obj.fn.call(window,10, 20), 1000);會立即執行,換成apply也是一樣,所以可以換成bind.
// 將bind在定時器中使用
// setTimeout(obj.fn.bind(window, 10, 20), 1000);
//=>setTimeout(anonymous, 1000); 1秒後先執行bind的返回結果anonymous函數,在anonymous中再把需要執行的obj.fn執行,把之前存儲的context/args傳遞給函數
// 若把這個bind給頁面的點擊事件:
// document.body.onclick = obj.fn.bind(window, 10, 20);
//=> document.body.onclick = anonymous;不僅會執行anonymous,還會默認給anonymous傳一個ev的參數
// call的源碼:
// function call(context = window, ...args) {
// context.$fn = this;
// let result = context.$fn(...args); //此時要執行的這個函數中,this是context
// return result;
// } => AAAFFF000可視爲堆內存
function fn1() { console.log(1) };
function fn2() { console.log(2) };
fn1.call(fn2);// => this是fn1, context:fn2,先執行call函數,在call中this是fn2,沒有用到,故不管 => 執行的是FN1 => 1
fn1.call.call(fn2); //=>執行的是FN2 =>2 不管幾個call,都是原型上的call方法執行
/*
多個CALL執行,執行過程:
先讓最後一個CALL執行
this=> fn1.call => 去原型上找call這個方法,就是對AAAFFF000
context=> fn2
args=>[]
fn2.$fn = AAAFFF000 fn2.$fn(...[]),沒有傳參,直接執行
讓call方法再執行:
this=> fn2
context=> undefined
undefined.$fn=fn2; undefined.$fn()
=>執行fn2 => 輸出2
*/
Function.prototype.call(fn1);
/*
先讓CALL執行
this=>Function.prototype(FN的prototype是anonymous匿名函數)
context=> fn1
args=>[]
fn1.$fn=Function.prototype fn1.$fn()
=> 讓fn1.$fn()執行,相當於Function.prototype執行,這是個匿名空函數,執行沒有返回值,所以沒有輸出
*/
Function.prototype.call.call(fn1); //=>執行FN1 =>1
/*
執行最後一個CALL
this=>Function.prototype.call(AAAFFF000)
context=>fn1
args=>[]
fn1.$fn=Function.prototype.call =>AAAFFF000 fn1.$fn()執行
再次執行CALL函數
this=>fn1
context=>undefined
args=[]
undefined.$fn=fn1 讓undefined.$fn()執行
=> 相當於執行fn1 => 1
*/
/*
在這總結下call的使用規律:
1個call,call左邊是誰,就執行誰
多個call,兩個及兩個以上call,第一個參數傳誰就讓誰執行
*/
</script>
這就是JS 中的五種THIS情況,記錄下,以便以後用到(很多執行過程寫在了代碼註釋中,便於理解)。
加油~
學過的每樣東西,都會派上用場