The last time, I have learnedjavascript
【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。html
也是給本身的查缺補漏和技術分享。前端
歡迎你們多多評論指點吐槽。java
系列文章均首發於公衆號【全棧前端精選】,筆者文章集合詳見Nealyang/personalBlog。目錄皆爲暫定node
講道理,這篇文章有些拿捏很差尺度。準確的說,這篇文章講解的內容基本算是基礎的基礎了,可是每每這種基礎類的文章很難在囉嗦和詳細中把持好。文中道不到的地方還望各位評論多多補充指正。react
相信使用過 JavaScript 庫作過開發的同窗對 this
都不會陌生。雖然在開發中 this
是很是很是常見的,可是想真正吃透 this
,其實仍是有些不容易的。包括對於一些有經驗的開發者來講,也都要駐足琢磨琢磨~ 包括想寫清楚 this 呢,其實還得聊一聊 JavaScript 的做用域和詞法git
function foo(num) {
console.log("foo:"+num);
this.count++;
}
foo.count = 0;
for(var i = 0;i<10;i++){
foo(i);
}
console.log(foo.count);
複製代碼
經過運行上面的代碼咱們能夠看到,foo
函數的確是被調用了十次,可是this.count
彷佛並無加到foo.count
上。也就是說,函數中的this.count並非foo.count。es6
另外一種對this的誤解是它不知怎麼的指向函數的做用域,其實從某種意義上來講他是正確的,可是從另外一種意義上來講,這的確是一種誤解。github
明確的說,this不會以任何方式指向函數的詞法做用域,做用域好像是一個將全部可用標識符做爲屬性的對象,這從內部來講他是對的,可是JavaScript代碼不能訪問這個做用域「對象」,由於它是引擎內部的實現web
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //undefined
複製代碼
既然是全局環境,咱們固然須要去明確下宿主環境
這個概念。簡而言之,一門語言在運行的時候須要一個環境,而這個環境的就叫作宿主環境
。對於 JavaScript 而言,宿主環境最爲常見的就是 web 瀏覽器。
如上所說,咱們也能夠知道環境不是惟一的,也就是 JavaScript 代碼不只僅能夠在瀏覽器中跑,也能在其餘提供了宿主環境
的程序裏面跑。另外一個最爲常見的就是 Node
了,一樣做爲宿主環境
,node
也有本身的 JavaScript
引擎:v8
.
this
等價於 window
對象var
聲明一個變量等價於給 this
或者 window
添加屬性var
或者let(ECMAScript 6)
,你就是在給全局的this
添加或者改變屬性值REPL
來執行程序,那麼 this
就等於 global
this
並不指向 global
而是module.exports
爲{}
REPL
執行一個腳本文件,用var聲明一個變量並不會和在瀏覽器裏面同樣將這個變量添加給thisnode
環境裏,用REPL運行腳本文件的時候,若是在聲明變量的時候沒有使用var
或者let
,這個變量會自動添加到global
對象,可是不會自動添加給this
對象。若是是直接執行代碼,則會同時添加給global
和this
這一塊代碼比較簡單,咱們不用碼說話,改成用圖說話吧!
不少文章中會將函數和方法區分開,可是我以爲。。。不必啊,咱就看誰點了如花這位菇涼就行
當一個函數被調用的時候,會創建一個活動記錄,也成爲執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何 被調用的,被傳遞了什麼參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。
函數中的 this 是多變的,可是規則是不變的。
你問這個函數:」老妹~ oh,不,函數!誰點的你?「
」是他!!!「
那麼,this 就指向那個傢伙!再學術化一些,因此!通常狀況下!this不是在編譯的時候決定的,而是在運行的時候綁定的上下文執行環境。this 與聲明無關!
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
複製代碼
記住上面說的,誰點的我!!!
=> foo()
= windwo.foo()
,因此其中this 執行的是 window 對象,天然而然的打印出來 2.
須要注意的是,對於嚴格模式來講,默認綁定全局對象是不合法的,this被置爲undefined。
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
複製代碼
雖然這位 xx 被點的多了。。。可是,咱們只問點他的那我的,也就是 ojb2
,因此 this.a
輸出的是 42.
注意,我這裏的點!不是你想的那個點哦,是運行時~
恩。。。這,就是從良了
仍是如上文說到的,this,咱們不看在哪定義,而是看運行時。所謂的構造函數,就是關鍵字new
打頭!
誰給我 new,我跟誰
其實內部完成了以下事情:
foo = "bar";
function testThis(){
this.foo = 'foo';
}
console.log(this.foo);
new testThis();
console.log(this.foo);
console.log(new testThis().foo)//自行嘗試
複製代碼
恩。。。這就是被包了
在不少書中,call、apply、bind 被稱之爲 this 的強綁定。說白了,誰出力,我跟誰。那至於這三者的區別和實現以及原理呢,我們下文說!
function dialogue () {
console.log (`I am ${this.heroName}`);
}
const hero = {
heroName: 'Batman',
};
dialogue.call(hero)//I am Batman
複製代碼
上面的dialogue.call(hero)
等價於dialogue.apply(hero)``dialogue.bind(hero)()
.
其實也就是我明確的指定這個 this 是什麼玩意兒!
箭頭函數的 this 和 JavaScript 中的函數有些不一樣。箭頭函數會永久地捕獲 this值,阻止 apply或 call後續更改它。
let obj = {
name: "Nealyang",
func: (a,b) => {
console.log(this.name,a,b);
}
};
obj.func(1,2); // 1 2
let func = obj.func;
func(1,2); // 1 2
let func_ = func.bind(obj);
func_(1,2);// 1 2
func(1,2);// 1 2
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);// 1 2
複製代碼
箭頭函數內的 this值沒法明確設置。此外,使用 call 、 apply或 bind等方法給 this傳值,箭頭函數會忽略。箭頭函數引用的是箭頭函數在建立時設置的 this值。
箭頭函數也不能用做構造函數。所以,咱們也不能在箭頭函數內給 this設置屬性。
雖然 JavaScript 是不是一個面向對象的語言至今還存在一些爭議。這裏咱們也不去爭論。可是咱們都知道,類,是 JavaScript 應用程序中很是重要的一個部分。
類一般包含一個 constructor , this能夠指向任何新建立的對象。
不過在做爲方法時,若是該方法做爲普通函數被調用, this也能夠指向任何其餘值。與方法同樣,類也可能失去對接收器的跟蹤。
class Hero {
constructor(heroName) {
this.heroName = heroName;
}
dialogue() {
console.log(`I am ${this.heroName}`)
}
}
const batman = new Hero("Batman");
batman.dialogue();
複製代碼
構造函數裏的 this指向新建立的 類實例。當咱們調用 batman.dialogue()時, dialogue()做爲方法被調用, batman是它的接收器。
可是若是咱們將 dialogue()方法的引用存儲起來,並稍後將其做爲函數調用,咱們會丟失該方法的接收器,此時 this參數指向 undefined 。
const say = batman.dialogue;
say();
複製代碼
出現錯誤的緣由是JavaScript 類是隱式的運行在嚴格模式下的。咱們是在沒有任何自動綁定的狀況下調用 say()函數的。要解決這個問題,咱們須要手動使用 bind()將 dialogue()函數與 batman綁定在一塊兒。
const say = batman.dialogue.bind(batman);
say();
複製代碼
咳咳,技術文章,我們嚴肅點
咱們都說,this指的是函數運行時所在的環境。可是爲何呢?
咱們都知道,JavaScript 的一個對象的賦值是將地址賦值給變量的。引擎在讀取變量的時候其實就是要了個地址而後再從原地址讀出來對象。那麼若是對象裏屬性也是引用類型的話(好比 function
),固然也是如此!
而JavaScript 容許函數體內部,引用當前環境的其餘變量,而這個變量是由運行環境提供的。因爲函數又能夠在不一樣的運行環境執行,因此須要個機制來給函數提供運行環境!而這個機制,也就是咱們說到心在的 this。this的初衷也就是在函數內部使用,代指當前的運行環境。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 單獨執行
f() // 1
// obj 環境執行
obj.f() // 2
複製代碼
obj.foo()
是經過obj
找到foo
,因此就是在obj
環境執行。一旦var foo = obj.foo
,變量foo
就直接指向函數自己,因此foo()
就變成在全局環境
執行.
var bar = new Foo();
複製代碼
var bar = foo.call(obj);
複製代碼
var bar = obj.foo();
複製代碼
var bar = foo();
複製代碼
var number = 2;
var obj = {
number: 4,
/*匿名函數自調*/
fn1: (function() {
var number;
this.number *= 2; //4
number = number * 2; //NaN
number = 3;
return function() {
var num = this.number;
this.number *= 2; //6
console.log(num);
number *= 3; //9
alert(number);
};
})(),
db2: function() {
this.number *= 2;
}
};
var fn1 = obj.fn1;
alert(number);
fn1();
obj.fn1();
alert(window.number);
alert(obj.number);
複製代碼
評論區留下你的答案吧~
上文中已經提到了 call
、apply
和 bind
,在 MDN
中定義的 apply
以下:
apply() 方法調用一個函數, 其具備一個指定的this值,以及做爲一個數組(或相似數組的對象)提供的參數
語法:
fun.apply(thisArg, [argsArray])
如上概念 apply
相似.區別就是 apply 和 call 傳入的第二個參數類型不一樣。
call 的語法爲:
fun.call(thisArg[, arg1[, arg2[, ...]]])
複製代碼
須要注意的是:
apply 的語法爲:
Function.apply(obj[,argArray])
複製代碼
須要注意的是:
記憶技巧:apply,a 開頭,array,因此第二參數須要傳遞數據。
請問!什麼是類數組?
借!
對,就是借。舉個栗子!我沒有女友,週末。。。額,不,我沒有摩托車🏍,週末的時候天氣很好,想出去壓彎。可是我有沒有錢!怎麼辦呢,找朋友借用一下啊~達到了目的,還節省開支!
放到程序中咱們能夠理解爲,某一個對象沒有想用的方法去實現某個功能,可是不想浪費內存開銷,就借用另外一個有該方法的對象去借用一下。
說白了,包括 bind,他們的核心理念都是借用方法,已達到節省開銷的目的。
代碼比較簡單,就不作講解了
const arrayLike = {
0: 'qianlong',
1: 'ziqi',
2: 'qianduan',
length: 3
}
const arr = Array.prototype.slice.call(arrayLike);
複製代碼
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
Math.max.apply(Math, arr);
Math.max.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
Math.min.apply(Math, arr);
Math.min.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
複製代碼
Object.prototype.toString
用來判斷類型再合適不過,尤爲是對於引用類型來講。
function isArray(obj){
return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([]) // true
isArray('qianlong') // false
複製代碼
// 父類
function supFather(name) {
this.name = name;
this.colors = ['red', 'blue', 'green']; // 複雜類型
}
supFather.prototype.sayName = function (age) {
console.log(this.name, 'age');
};
// 子類
function sub(name, age) {
// 借用父類的方法:修改它的this指向,賦值父類的構造函數裏面方法、屬性到子類上
supFather.call(this, name);
this.age = age;
}
// 重寫子類的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
sonFn.prototype = Object.create(fatherFn.prototype); // 繼承父類的屬性以及方法
sonFn.prototype.constructor = sonFn; // 修正constructor指向到繼承的那個函數上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
console.log(this.age, 'foo');
};
// 實例化子類,能夠在實例上找到屬性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}
複製代碼
繼承後面可能也會寫一個篇【THE LAST TIME】。也是比較基礎,不知道有沒有這個必要
簡易版繼承
ar Person = function (name, age) {
this.name = name;
this.age = age;
};
var Girl = function (name) {
Person.call(this, name);
};
var Boy = function (name, age) {
Person.apply(this, arguments);
}
var g1 = new Girl ('qing');
var b1 = new Boy('qianlong', 100);
複製代碼
bind 和 call/apply 用處是同樣的,可是 bind
會**返回一個新函數!不會當即執行!**而call/apply
改變函數的 this 而且當即執行。
原理其實就是返回閉包,畢竟 bind 返回的是一個函數的拷貝
for (var i = 1; i <= 5; i++) {
// 緩存參數
setTimeout(function (i) {
console.log('bind', i) // 依次輸出:1 2 3 4 5
}.bind(null, i), i * 1000);
}
複製代碼
上述代碼也是一個經典的面試題,具體也不展開了。
說道 this 丟失問題,應該最多見的就是 react 中定義一個方法而後後面要加 bind(this)
的操做了吧!固然,箭頭函數不須要,這個我們上面討論過。
第一個手寫我們一步一步來
Function.prototype.NealApply = function(context,args){}
複製代碼
Function.prototype.NealApply = function(context,args){
context = context || window;
args = args || [];
}
複製代碼
對,咱們沒有黑魔法,既然綁定 this,仍是逃不掉咱們上文說的那些 this 方式
Function.prototype.NealApply = function(context,args){
context = context || window;
args = args || [];
//給context新增一個獨一無二的屬性以避免覆蓋原有屬性
const key = Symbol();
context[key] = this;//這裏的 this 是函數
context[key](...args);
}
複製代碼
其實這個時候咱們用起來已經有效果了。
Function.prototype.NealApply = function(context,args){
context = context || window;
args = args || [];
//給context新增一個獨一無二的屬性以避免覆蓋原有屬性
const key = Symbol();
context[key] = this;//這裏的 this 是 testFun
const result = context[key](...args);
// 帶走產生的反作用
delete context[key];
return result;
}
var name = 'Neal'
function testFun(...args){
console.log(this.name,...args);
}
const testObj = {
name:'Nealyang'
}
testFun.NealApply(testObj,['一塊兒關注',':','全棧前端精選']);
複製代碼
執行結果就是上方的截圖。
一上來不說優化是由於但願你們把精力放到核心,而後再去修邊幅! 羅馬不是一日建成的,看別人的代碼多牛批,其實也是一點一點完善出來的。
道理是這麼個道理,其實要作的優化還有不少,這裏咱們就把 context 的判斷須要優化下:
// 正確判斷函數上下文對象
if (context === null || context === undefined) {
// 指定爲 null 和 undefined 的 this 值會自動指向全局對象(瀏覽器中爲window)
context = window
} else {
context = Object(context) // 值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的實例對象
}
複製代碼
別的優化你們能夠添加各類的用戶容錯。好比對第二個參數的類數組作個容錯
function isArrayLike(o) {
if (o && // o不是null、undefined等
typeof o === 'object' && // o是對象
isFinite(o.length) && // o.length是有限數值
o.length >= 0 && // o.length爲非負值
o.length === Math.floor(o.length) && // o.length是整數
o.length < 4294967296) // o.length < 2^32
return true;
else
return false;
}
複製代碼
打住!真的再也不多囉嗦了,這篇文章篇幅不該這樣的
丐版實現:
//傳遞參數從一個數組變成逐個傳參了,不用...擴展運算符的也能夠用arguments代替
Function.prototype.NealCall = function (context, ...args) {
//這裏默認不傳就是給window,也能夠用es6給參數設置默認參數
context = context || window;
args = args ? args : [];
//給context新增一個獨一無二的屬性以避免覆蓋原有屬性
const key = Symbol();
context[key] = this;
//經過隱式綁定的方式調用函數
const result = context[key](...args);
//刪除添加的屬性
delete context[key];
//返回函數調用的返回值
return result;
}
複製代碼
bind的實現講道理是比 apply 和call 麻煩一些的,也是面試頻考題。由於須要去考慮函數的拷貝。可是也仍是比較簡單的,網上也有不少版本,這裏就不具體展開了。具體的,我們能夠在羣裏討論~
Function.prototype.myBind = function (objThis, ...params) {
const thisFn = this; // 存儲源函數以及上方的params(函數參數)
// 對返回的函數 secondParams 二次傳參
let fToBind = function (...secondParams) {
const isNew = this instanceof fToBind // this是不是fToBind的實例 也就是返回的fToBind是否經過new調用
const context = isNew ? this : Object(objThis) // new調用就綁定到this上,不然就綁定到傳入的objThis上
return thisFn.call(context, ...params, ...secondParams); // 用call調用源函數綁定this的指向並傳遞參數,返回執行結果
};
if (thisFn.prototype) {
// 複製源函數的prototype給fToBind 一些狀況下函數沒有prototype,好比箭頭函數
fToBind.prototype = Object.create(thisFn.prototype);
}
return fToBind; // 返回拷貝的函數
};
複製代碼
Function.prototype.myBind = function (context, ...args) {
const fn = this
args = args ? args : []
return function newFn(...newFnArgs) {
if (this instanceof newFn) {
return new fn(...args, ...newFnArgs)
}
return fn.apply(context, [...args,...newFnArgs])
}
}
複製代碼
別忘記了上面 this 的考覈題目啊,同窗,該交卷了!
關注公衆號: 【全棧前端精選】 每日獲取好文推薦。還能夠入羣,一塊兒學習交流