this 的概念與指向
this 的概念
通常來講,this 的值是在執行的時候才能確認的,定義的時候不能確認。因為 this 是執行上下文環境的一部分,而執行上下文需要在程式碼執行前確定,而不是定義的時候。所以 this 永遠指向最後呼叫它的那個物件。
但,這只是通常來講。apply、call、bind、箭頭函式都會改變 this 的指向。
作為一個函式呼叫
// 情況1
function foo() {
console.log(this.a) //1
}
var a = 1
foo() //this -> window
複製程式碼
作為方法呼叫
// 情況2
function fn(){
console.log(this);
}
var obj={fn:fn};
obj.fn(); //this -> obj
複製程式碼
建構函式中
// 情況3
function CreateJsPerson(name,age){
// this是當前類的一個例項p1
this.name = name; // => p1.name=name
this.age = age; // => p1.age=age
}
var p1 = new CreateJsPerson("尹華芝",48);
複製程式碼
call、apply、bind
// 情況4
function add(c, d){
return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16 //這裡使用了 call 對 this 進行了重定向
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34 //這裡使用了 apply 對 this 進行了重定向
複製程式碼
箭頭函式
// 情況5
<button id="btn1">箭頭函式this</button>
<script type="text/javascript">
let btn1 = document.getElementById('btn1');
let obj = {
name: 'kobe',
age: 39,
getName: function () {
btn1.onclick = () => {
console.log(this);//obj
};
}
};
obj.getName();
</script>
複製程式碼
箭頭函式在自己的作用域內不繫結 this
,即沒有自己的 this
,如果要使用 this
,就會指向定義時所在的作用域的 this
值。
在 《ES6 標準入門中》:箭頭函式的this
,總是指向定義時所在的物件,而不是執行時所在的物件。
由於箭頭函式的 this 是在定義時確定的,所以我們不能在建構函式中使用箭頭函式,建構函式的 this 要指向例項才行,因此只能使用一般的函式。
總結
- 對於直接呼叫 foo 來說,不管 foo 函式被放在了生命地方,this 一定是 window
- 對於 obj.foo() 來說,我們要記住,誰呼叫了函式,誰就是 this,所以在這個場景下,foo 函式中的 this 就是 obj 物件
- 在建構函式模式中,類中(函式體中)出現的 this.xxx = xxx 中的 this 就是當前類的一個例項
- call、apply、bind 的 this 是第一個引數(call 的接收和 apply 不同,apply 接收陣列,而 call 則是用“,”分隔,進行接收)
- 箭頭函式沒有自己的this,看其外層是否有函式,如果有,外層函式的 this 就是內部箭頭函式的 this,如果沒有,this 就是 window。需要注意的是:箭頭函式的 this 始終指向函式定義時的 this,而非執行時。
call、apply 與 bind
call、apply、bind
本質都是改變 this
的指向,不同點 call、apply
是直接呼叫函式,bind
是返回一個新的函式。call
跟 apply
就只有引數上不同。
call 手寫程式碼
call() 讓函式執行,第一個引數讓 this 的指向改為傳進去的引數,後面的當引數傳進函式裡面。
返回值為原函式的返回值,如果不傳第一個引數為 this 就指向 window。
ES6 版:
Function.prototype.ca112 = function (context, ...arrs ){
context = context || window; // 因為傳遞過來的 context 很可能是 null
context.fn = this; // 讓 fn 的上下文是 context
const result = context.fn(.. .arrs);
delete context.fn;
return result;
}
複製程式碼
ES5 版:
Function.prototype. call2 = function (context) {
var context = context || window; // 因為傳遞過來的 context 很可能是 null
context.fn = this;
var args = [];
for (var i = 1; i< arguments.length; i++){
// 不這樣的話,字串的引號會被去掉,變成變數
args.push("arguments[" + i + "]");
}
args = args.join(","); // 把陣列變成字串
// 相當於執行 context.fn(arguments[1], arguments[2])
var result = eval("contest.fn(" + args + ")");
delete context.fn;
return result;
}
複製程式碼
ES5 版本的使用 eval 來執行語句,這樣會又一定的效能影響,但是這樣做相容性好
因為不知道會輸入多少個,所以這裡直接使用 arguments 來遍歷好了,先把 arguments 轉成陣列,再轉成字串,然後利用 eval 執行程式碼(看見網上說 eval 有安全性問題,不過這裡這樣就夠了。)
apply 手寫程式碼
ES6 版:
Function.prototype.apply2 = function(context, arr) {
context = context || window; // 因為傳遞的可能是 null
context.fn = this; // 讓 fn 的上下文成為 context
arr = arr || [];
const result = context.fn(...arr);
delete context.fn;
return result; // 因為有可能 this 函式會有返回值
};
複製程式碼
ES5 版:
Function.prototype.apply2 = function(context, arr) {
var context = context || window;
context.fn = this;
var args = [];
var params = arr || [];
for(var i = 0; i < params.length; i++) {
args.push("params[" + i + "]");
}
args = args.join(",");
var result = eval("contest.fn(" + args + ")");
delete context.fn;
return result;
}
複製程式碼
bind 手寫程式碼
bind
是封裝了 call
的方法改變了 this
的指向並返回一個新的函式
ES6 版:
Function.prototype.bind2 = function(context, ...arrs) {
let _this = this;
return function() {
_this.call(context, ...arrs, ...arguments);
}
}
複製程式碼
ES5 版:
Function.prototype.bind2 = function(context) {
var _this = this;
var argsParent = Array.prototype.slice.call(arguments, 1);
return function() {
var args = argsParent.concat(Array.prototype.slice.call(arguments));
_this.apply(context, args);
};
}
複製程式碼
三者的同異性總結
call、apply、bind
- 三者都是用來改變函式的 this 物件的指向的
- 第一個引數都是 this 要指向的物件
- 都可以利用後續引數進行傳參
- 引數傳遞
call 方法傳參是傳一個或多個引數,第一個引數是指定的物件
func.call(thisArg,arg1,arf2,……)
複製程式碼
apply 方法傳參是傳一個或兩個物件,第一個引數是指定的物件,第二個引數是一個陣列或類陣列(說到類陣列就想起了 arguments)
func.apply(thisArg,【argsArray】)
複製程式碼
bind 方法傳參是傳一個或者多個引數,跟 call 方法傳遞引數一樣。
func.bind(this.thisArg,arg1,arg2,arg3……)
複製程式碼
- 呼叫後是否立即執行
call 和 apply 在函式呼叫它們之後,就會立即執行這個函式;
而函式呼叫了 bind 後,會返回撥用函式的引用,如果要執行的話,需要執行返回函式的引用。
let name = 'window name';
let obj = {
name: 'call_me_R'
};
function sayName() {
console.log(this.name);
}
sayName(); // window name
sayName.call(obj); // call_me_R
sayName.apply(obj); // call_me_R
let _sayName = sayName.bind(obj);
_syaName(); // call_me_R
複製程式碼
執行的區別在與 ball 和 apply 都是立即執行的,bind 會返回回撥函式,手動執行回撥函式以執行。
new 與 Object.create()
new 做的事情
New 關鍵字會進行如下的操作
-
建立一個空的簡單 JavaScript 物件(即 {})
-
連結該物件(即設定該物件的建構函式)到另一個物件
-
將建立的新的物件作為 this 的上下文
-
如果該函式沒有返回物件,則返回 this
-
new 會建立一個新的物件,並且這個新物件繼承建構函式的 prototype,也就是說建立的例項的 proto 指向建構函式的 prototype
-
new Object()會建立一個例項,該例項的 proto 指向 Object 的 prototype
手寫 new 四個步驟:
- 建立一個空物件,並且 this 變數引用該物件
- 繼承函式的原型
- 屬性和方法加入 this 引用的物件中,並執行函式
- 新建立的物件有 this 所引用,並且最後隱式返回 this
function _new(func){
let target = {},
target.__proto__ = func.prototype;
let res = func.call(target);
if(typeof(res)=='object' || typeof(res)=='function'){
return res;
}
return target;
}
複製程式碼
簡單來說:
new 做的三件事情:
- 指定 prototype
- 用 call 呼叫物件
- 返回 this
function myNew (fun) {
return function () {
// 建立一個新物件且將其隱式原型指向建構函式原型
let obj = {
__proto__ : fun.prototype
}
// 執行建構函式
fun.call(obj, ...arguments)
// 返回該物件
return obj
}
}
function person(name, age) {
this.name = name
this.age = age
}
let obj = myNew(person)('chen', 18) // {name: "chen", age: 18}
複製程式碼
Object.Create() 基本實現及其原理
// 思路:將傳入的物件作為原型
function create(obj) {
function F() {}
F.prototype = obj
return new F()
}
複製程式碼
Object.Create 是建立了一個新的物件並返回,這個新物件的原型指向了拷貝的物件,當我們通過 b.a 訪問 obj.a 時,是通過原型進行訪問的。
但是要注意的是,Object.Create 並不是深拷貝,Object.Create() 新建的物件共享的是拷貝的物件的引用型別的地址(淺拷貝)。
所以如果修改的是引用型別,還是會變化。
由於 Object.create() 還可以傳遞第二個引數,所以更好的實現是:
function myCreate(proto, properties) {
// 新物件
let fn = function() {};
fn.prototype = proto;
if(properties) {
// defineProperties 在新物件上定義新的屬性或修改現有屬性
Object.defineProperties(fn, properties)
}
return new fn();
}
複製程式碼
new 與 object.create 的異同
- new Object() 繼承內建物件 Object,Object.create 繼承指定物件
- 可以通過 Object.create(null) 建立一個乾淨的物件,也就是沒有原型,而 new Object() 建立的物件是 Object 的例項,原型永遠指向 Object.prototype
Object.create 接受兩個引數,即 object.create(proto,propertiesObject)
proto:現有的物件,即新物件的原型物件(新建立的物件 proto 將指向該物件)。如果 proto 為 null,那麼建立出來的物件是一個 {} 並且沒有原型。
propertiesObject 可選,給新物件新增新屬性以及描述器。如果沒有指定即建立一個 {},有原型也有繼承 Object.prototype 上的方法。可參考 Object.defineProperties()的第二個引數。