Create by jsliang on 2019-2-21 08:42:02
Recently revised in 2019-2-23 09:44:08
Hello 小夥伴們,如果覺得本文還不錯,記得給個 star , 你們的 star 是我學習的動力!GitHub 地址
本文涉及知識點:
prototype
__proto__
new
call()
/apply()
/bind()
this
在本文中,jsliang 會講解通過自我探索後關於上述知識點的個人理解,如有紕漏、疏忽或者誤解,歡迎各位小夥伴留言指出。
如果小夥伴對文章存有疑問,想快速得到回覆。
或者小夥伴對 jsliang 個人的前端文件庫感興趣,也想將自己的前端知識整理出來。
歡迎加 QQ 群一起探討:798961601
。
一 目錄
不折騰的前端,和鹹魚有什麼區別
目錄 |
---|
一 目錄 |
二 前言 |
三 題目 |
四 解題 |
五 知識擴充 |
5.1 問題少年:旅途開始 |
5.2 原型及原型鏈 |
5.3 new 為何物 |
5.4 call() 又是啥 |
5.5 this 指向哪 |
六 總結 |
七 參考文獻 |
八 工具 |
二 前言
廣州小夥伴在幫我進行面試摸底的時候,提出了問題:能否談談 this 的作用?
題目的目的:
- 瞭解 this,說一下 this 的作用。
- Vue 的 this.變數,this 指向 Vue 的哪裡。(指 Vue 的例項)
- Vue 裡寫個 setTimeout,發現 this 改變(
call()
、apply()
、=>
) - ……大致如此……
但是,我發現了我走了一條不歸路,無意間我看了下 prototype
!
然後,我爬上了一座高山……
三 題目
相信有的小夥伴能自信地做出下面這些題~
- 題目 1
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
}
var c = new A();
console.log(b.n);
console.log(b.m);
console.log(c.n);
console.log(c.m);
複製程式碼
請寫出上面程式設計的輸出結果是什麼?
- 題目 2
var F = function() {};
Object.prototype.a = function() {
console.log('a');
};
Function.prototype.b = function() {
console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b();
複製程式碼
請寫出上面程式設計的輸出結果是什麼?
- 題目 3
function Person(name) {
this.name = name
}
let p = new Person('Tom');
複製程式碼
問題1:1. p.__proto__等於什麼?
問題2:Person.__proto__等於什麼?
- 題目 4
var foo = {},
F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';
console.log(foo.a);
console.log(foo.b);
console.log(F.a);
console.log(F.b);
複製程式碼
請寫出上面程式設計的輸出結果是什麼?
四 解題
- 題目 1 答案:
b.n -> 1
b.m -> undefined;
c.n -> 2;
c.m -> 3;
複製程式碼
- 題目 2 答案:
f.a() -> a
f.b() -> f.b is not a function
F.a() -> a
F.b() -> b
複製程式碼
- 題目 3 答案
答案1:Person.prototype
答案2:Function.prototype
- 題目 4 答案
foo.a => value a
foo.b => undefined
F.a => value a
F.b => value b
複製程式碼
如果小夥伴們檢視完答案,仍不知道怎麼回事,那麼,我們擴充套件下自己的知識點,暢快了解更多地知識吧!
五 知識擴充
原型和原型鏈估計是老生常談的話題了,但是還是有很多小白(例如 jsliang 自己)就時常懵逼在這裡。
首圖祭祖,讓暴風雨來得更猛烈些吧!
5.1 問題少年:旅途開始
因為愛(瞭解來龍去脈),所以 jsliang 開始學習(百度)之旅,瞭解原型和原型鏈。
首先,jsliang 去了解檢視原型鏈 prototype
。
然後,在瞭解途中看到了 new
,於是百度檢視 JS 的 new
理念。
接著,接觸 new
會了解還有 call()
,而 call()
、apply()
以及箭頭函式 =>
又是相似的東西。
最後,當我們查詢 call()
的時候,它又涉及到了 this
,所以我們 “順便” 查閱 this
吧。
5.1 原型及原型鏈
首先,為什麼需要原型及原型鏈?
我們檢視一個例子:
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function() {
console.log(age + "歲的" + name + "在吃飯。");
}
}
let p1 = new Person("jsliang", 24);
let p2 = new Person("jsliang", 24);
console.log(p1.eat === p2.eat); // false
複製程式碼
可以看到,對於同一個函式,我們通過 new
生成出來的例項,都會開出新的一塊堆區,所以上面程式碼中 person 1 和 person 2 的吃飯是不同的。
擁有屬於自己的東西(例如房子、汽車),這樣很好。但它也有不好,畢竟總共就那麼點地兒(記憶體),你不停地建房子,到最後是不是沒有空地了?(記憶體不足)
所以,我們要想個法子,建個類似於共享庫的物件(例如把樓房建高),這樣就可以在需要的時候,呼叫一個類似共享庫的物件(社群),讓例項能夠沿著某個線索去找到自己歸處。
而這個線索,在前端中就是原型鏈 prototype
。
function Person(name) {
this.name = name;
}
// 通過建構函式的 Person 的 prototype 屬性找到 Person 的原型物件
Person.prototype.eat = function() {
console.log("吃飯");
}
let p1 = new Person("jsliang", 24);
let p2 = new Person("樑峻榮", 24);
console.log(p1.eat === p2.eat); // true
複製程式碼
看!這樣我們就通過分享的形式,讓這兩個例項物件指向相同的位置了(社群)。
然後,說到這裡,我們就興趣來了,prototype
是什麼玩意?居然這麼神奇!
孩子沒娘,說來話長。首先我們要從 JavaScript 這玩意的誕生說起,但是放這裡的話,故事主線就太長了,所以這裡有個本文的劇場版《JavaScript 世界萬物誕生記》,感興趣的小夥伴可以去了解一下。這裡我們還是看圖,並回歸本話題:
- JS 說,我好寂寞。因為 JS 的本源是空的,即:null。
- JS 說,要有神。所以它通過萬能術
__proto__
產生了 No1 這號神,即:No1.__proto__ == null
。 - JS 說,神你要有自己的想法啊。所以神自己想了個方法,根據自己的原型
prototype
建立了物件Object
,即:Object.prototype == No1; No1.__proto__ == null
。於是我們把prototype
叫做原型,就好比Object
的原型是神,男人的原型是人類一樣,同時__proto__
叫做原型鏈,畢竟有了__proto__
,物件、神、JS 之間才有聯絡。這時候Object.prototype.__proto__ == null
。 - JS 說,神你要有更多的想法啊,我把萬能術
__proto__
借你用了。所以神根據Object
,使用__proto__
做了個機器 No2,即No2.__proto__ == No1
,並規定所有的東西,通過__proto__
可以連線機器,再找到自己,包括Object
也是,於是 Object 成為所有物件的原型,Object.__proto__.__proto__ == No1
,然後String
、Number
、Boolean
、Array
這些物種也是如此。 - JS 說,神你的機器好厲害喔!你的機器能不能做出更多的機器啊?神咧嘴一笑:你通過萬能術創造了我,我通過自己原型創造了物件。如此,那我造個機器 Function,
Function.prototype == No2, Function.__proto__ == No2
,即Function.prototype == Function.__proto__
吧!這樣 No2 就成了造機器的機器,它負責管理 Object、Function、String、Number、Boolean、Array 這幾個。
最後,說到這裡,我們應該很瞭解開局祭祖的那副圖,並有點豁然開朗的感覺,能清楚地瞭解下面幾條公式了:
Object.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
複製程式碼
5.3 new 為何物
這時候,我們知道 prototype
以及 __proto__
是啥了,讓我們迴歸之前的程式碼:
function Person(name) {
this.name = name;
}
// 通過建構函式的 Person 的 prototype 屬性找到 Person 的原型物件
Person.prototype.eat = function() {
console.log("吃飯");
}
let p1 = new Person("jsliang", 24);
let p2 = new Person("樑峻榮", 24);
console.log(p1.eat === p2.eat); // true
複製程式碼
可以看出,這裡有個點,我們還不清楚,就是:new 為何物?
首先,我們來講講函式:函式分為建構函式和普通函式。
怎麼回事呢?No2 始機器 在創造機器 Function 的過程中,創造了過多的機器,為了方便區分這些機器,No1 神 將機器分為兩類:創造物種類的 Function 叫做建構函式(通常物件導向),創造動作類的 Function 叫做普通函式(通常程式導向)。打個比喻:function Birl() {}
、function Person() {}
這類以首字母大寫形式來定義的,用來定義某個型別物種的,就叫做 建構函式。而 function fly() {}
、function eat() {}
這類以首字母小寫形式來定義的,用來定義某個動作的,就叫做普通函式。
注意,它們本質還是 Function 中出來的,只是為了方便區分,我們如此命名
然後,我們嘗試製作一個會飛的鳥:
// 定義鳥類
function Bird(color) {
this.color = color;
}
// 定義飛的動作
function fly(bird) {
console.log(bird + " 飛起來了!");
}
複製程式碼
接著,我們要使用鳥類這個機器創造一隻鳥啊,No1 神 撓撓頭,折騰了下(注意它折騰了下),跟我們說使用 new
吧,於是:
// 定義鳥類
function Bird(color) {
this.color = color;
}
// 創造一隻鳥
let bird1 = new Bird('藍色');
// 定義飛的動作
function fly(bird) {
console.log(bird.color + "的鳥飛起來了!");
}
fly(bird1); // 藍色的鳥飛起來了!
複製程式碼
說到這裡,我們知道如何使用型別創造機器和動作創造機器了。
最後,我們如果有興趣,還可以觀察下 No1 神 在 new
內部折騰了啥:
假如我們使用的是:let bird1 = new Bird('藍色');
// 1. 首先有個型別機器
function ClassMachine() {
console.log("型別創造機器");
}
// 2. 然後我們定義一個物件物品
let thingOne = {};
// 3. 物件物品通過萬能術 __proto__ 指向了型別機器的原型(即 No 2 始機器)
thingOne.__proto__ = ClassMachine.prototype;
// 4. ???
ClassMachine.call(thingOne);
// 5. 定義了型別機器的動作
ClassMachine.prototype.action = function(){
console.log("動作創造機器");
}
// 6. 這個物件物品執行了動作
thingOne.action();
/*
* Console:
* 型別創造機器
* 動作創造機器
*/
複製程式碼
OK,new
做了啥,No 1 神安排地明明白白了。
那麼下面這個例子,我們也就清楚了:
function Person(name){
this.name = name
}
Person.prototype = {
eat:function(){
console.log('吃飯')
},
sleep:function(){
console.log('睡覺')
}
};
let p = new Person('樑峻榮',28);
// 訪問原型物件
console.log(Person.prototype);
console.log(p.__proto__); // __proto__僅用於測試,不能寫在正式程式碼中
/* Console
* {eat: ƒ, sleep: ƒ}
* {eat: ƒ, sleep: ƒ}
*/
複製程式碼
所以很多人會給出一條公式:
例項的 __proto__
屬性(原型)等於其建構函式的 prototype
屬性。
現在理解地妥妥的了吧!
但是,你注意到 new
過程中的點 4 了嗎?!!!
5.4 call() 又是啥
在點 4 中,我們使用了 call()
這個方法。
那麼,call()
又是啥?
首先,我們要知道 call()
方法是存在於 Funciton
中的,Function.prototype.call
是 ƒ call() { [native code] }
,小夥伴可以去控制檯列印一下。
然後,我們觀察下面的程式碼:
function fn1() {
console.log(1);
this.num = 111;
this.sayHey = function() {
console.log("say hey.");
}
}
function fn2() {
console.log(2);
this.num = 222;
this.sayHello = function() {
console.log("say hello.");
}
}
fn1.call(fn2); // 1
fn1(); // 1
fn1.num; // undefined
fn1.sayHey(); // fn1.sayHey is not a function
fn2(); // 2
fn2.num; // 111
fn2.sayHello(); // fn2.sayHello is not a function
fn2.sayHey(); //say hey.
複製程式碼
通過 fn1.call(fn2)
,我們發現 fn1
、fn2
都被改變了,call()
就好比一個小三,破壞了 fn1
和 fn2
和睦的家庭。
現在,fn1
除了列印自己的 console,其他的一無所有。而 fn2
擁有了 fn1
console 之外的所有東西:num
以及 sayHello
。
記住:在這裡,
call()
改變了 this 的指向。
然後,我們應該順勢看下它原始碼,搞懂它究竟怎麼實現的,但是 jsliang 太菜,看不懂網上關於它原始碼流程的文章,所以我們們還是多上幾個例子,搞懂 call()
能做啥吧~
- 例子 1:
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
let food1 = new Food('chees', 5);
food1; // Food {name: "chees", price: 5, category: "food"}
複製程式碼
可以看出,通過在 Food
構造方法裡面呼叫 call()
,成功使 Food
擴充了 name
以及 price
這兩個欄位。所以:
準則一:可以使用 call()
方法呼叫父建構函式。
- 例子 2:
var animals = [
{
species: 'Lion',
name: 'King'
},
{
species: 'Whale',
name: 'Fail'
}
]
for(var i = 0; i < animals.length; i++) {
(function(i) {
this.print = function() {
console.log('#' + i + ' ' + this.species + ": " + this.name);
}
this.print();
}).call(animals[i], i);
}
// #0 Lion: King
// #1 Whale: Fail
複製程式碼
可以看到,在匿名函式中,我們通過 call()
,成功將 animals
中的 this
指向到了匿名函式中,從而迴圈列印出了值。
準則二:使用 call()
方法呼叫匿名函式。
- 例子 3:
function greet() {
var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
console.log(reply);
}
var obj = {
animal: 'cats',
sleepDuration: '12 and 16 hours'
};
greet.call(obj); // cats typically sleep between 12 and 16 hours
複製程式碼
準則三:使用 call()
方法呼叫函式並且指定上下文的 this
。
最後,講到這裡,小夥伴們應該知道 call()
的一些用途了。
說到 call()
,我們還要講講跟它相似的 apply()
,其實這兩者都是相似的,只是 apply()
呼叫的方式不同,例如:
function add(a, b){
return a + b;
}
function sub(a, b){
return a - b;
}
// apply() 的用法
var a1 = add.apply(sub, [4, 2]); // sub 呼叫 add 的方法
var a2 = sub.apply(add, [4, 2]);
a1; // 6
a2; // 2
// call() 的用法
var a1 = add.call(sub, 4, 2);
複製程式碼
是的,apply()
只能呼叫兩個引數:新 this
物件和一個陣列 argArray
。即:function.call(thisObj, [arg1, arg2]);
以上,我們知道 apply()
和 call()
都是為了改變某個函式執行時的上下文而存在的(就是為了改變函式內部的 this
指向)。然後,因為這兩個方法會立即呼叫,所以為了彌補它們的缺失,還有個方法 bind()
,它不會立即呼叫:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>call()、apply() 以及 bind()</title>
</head>
<body>
<div id="box">我是一個盒子!</div>
<script>
window.onload = function() {
var fn = {
num: 2,
fun: function() {
document.getElementById("box").onclick = (function() {
console.log(this.num);
}).bind(this);
// }).call(this);
// }).apply(this);
}
/*
* 這裡的 this 是 fun,所以可以正確地訪問 num,
* 如果使用 bind(),會在點選之後列印 2;
* 如果使用 call() 或者 apply(),那麼在重新整理網頁的時候就會列印 2
*/
}
fn.fun();
}
</script>
</body>
</html>
複製程式碼
再回想下,為什麼會有 call()
、apply()
呢,我們還會發現它牽扯上了 this
以及箭頭函式(=>
),所以下面我們來了解了解吧~
5.5 this 指向哪
- 在絕大多數情況下,函式的呼叫方式決定了
this
的值。它在全域性執行環境中this
都指向全域性物件
怎麼理解呢,我們舉個例子:
// 在瀏覽器中, window 物件同時也是全域性物件
conosle.log(this === window); // true
a = 'apple';
conosle.log(window.a); // apple
this.b = "banana";
console.log(window.b); // banana
console.log(b); // banana
複製程式碼
但是,日常工作中,大多數的 this
,都是在函式內部被呼叫的,而:
- 在函式內部,
this
的值取決於函式被呼叫的方式。
function showAge(age) {
this.newAge = age;
console.log(newAge);
}
showAge("24"); // 24
複製程式碼
然而,問題總會有的:
- 一般
this
指向問題,會發生在回撥函式中。所以我們在寫回撥函式時,要注意一下this
的指向問題。
var obj = {
birth: 1995,
getAge: function() {
var b = this.birth; // 1995;
var fn = function() {
return this.birth;
// this 指向被改變了!
// 因為這裡重新定義了個 function,
// 假設它內部有屬於自己的 this1,
// 然後 getAge 的 this 為 this2,
// 那麼,fn 當然奉行就近原則,使用自己的 this,即:this1
};
return fn();
}
}
obj.getAge(); // undefined
複製程式碼
在這裡我們可以看到, fn
中的 this
指向變成 undefined
了。
當然,我們是有補救措施的。
首先,我們使用上面提及的 call()
:
var obj = {
birth: 1995,
getAge: function() {
var b = this.birth; // 1995
var fn = function() {
return this.birth;
};
return fn.call(obj); // 通過 call(),將 obj 的 this 指向了 fn 中
}
}
obj.getAge(); // 1995
複製程式碼
然後,我們使用 that
來接盤 this
:
var obj = {
birth: 1995,
getAge: function() {
var b = this.birth; // 1995
var that = this; // 將 this 指向丟給 that
var fn = function() {
return that.birth; // 通過 that 來尋找到 birth
};
return fn();
}
}
obj.getAge(); // 1995
複製程式碼
我們通過了 var that = this
,成功在 fn
中引用到了 obj
的 birth
。
最後,我們還可以使用箭頭函式 =>
:
var obj = {
birth: 1995,
getAge: function() {
var b = this.birth; // 1995
var fn = () => this.birth;
return fn();
}
}
obj.getAge(); // 1995
複製程式碼
講到這裡,我們再回首 new
那塊我們不懂的程式碼:
// 1. 首先有個型別機器
function ClassMachine() {
console.log("型別創造機器");
}
// 2. 然後我們定義一個物件物品
let thingOne = {};
// 3. 物件物品通過萬能術 __proto__ 指向了型別機器的原型(即 No 2 始機器)
thingOne.__proto__ = ClassMachine.prototype;
// 4. ???
ClassMachine.call(thingOne);
// 5. 定義了型別機器的動作
ClassMachine.prototype.action = function(){
console.log("動作創造機器");
}
// 6. 這個物件物品執行了動作
thingOne.action();
/*
* Console:
* 型別創造機器
* 動作創造機器
*/
複製程式碼
很容易理解啊,在第四步中,我們將 ClassMachine
的 this
變成了 thingOne
的 this
了!
以上,是不是感覺鬼門關走了一遭,終於成功見到了曙光!!!
六 總結
在開始的時候,也許有的小夥伴,看著看著會迷暈了自己!
不要緊,我也是!
當我跟著自己的思路,一步一步敲下來之後,我才發覺自己彷彿打通了任督二脈,對一些題目有了自己的理解。
所以,最重要的還是 折騰 啦!
畢竟:
不折騰的前端,和鹹魚有什麼區別!
七 參考資料
下面列舉本文精選參考文章,其中一些不重要的零零散散 30 來篇文章已被刷選。
- 《JavaScript 世界萬物誕生記》
- 《小邵教你玩轉JS物件導向》
- 《js中的new()到底做了些什麼??》
- 《MDN Function.prototype.call()》
- 《JavaScript中的call、apply、bind深入理解》
- 《箭頭函式 - 廖雪峰》
八 工具
jsliang 廣告推送:
也許小夥伴想了解下雲伺服器
或者小夥伴想買一臺雲伺服器
或者小夥伴需要續費雲伺服器
歡迎點選 雲伺服器推廣 檢視!
jsliang 的文件庫 由 樑峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的作品創作。
本許可協議授權之外的使用許可權可以從 creativecommons.org/licenses/by… 處獲得。