Create by jsliang on 2019-2-21 08:42:02
Recently revised in 2019-2-23 09:44:08javascript
Hello 小夥伴們,若是以爲本文還不錯,記得給個 star , 大家的 star 是我學習的動力!GitHub 地址php
【2019-08-16】Hello 小夥伴們,因爲 jsliang 對文檔庫進行了重構,這篇文章的一些連接可能失效,而 jsliang 沒有精力維護掘金這邊的舊文章,對此深感抱歉。請須要獲取最新文章的小夥伴,點擊上面的 GitHub 地址,去文檔庫查看調整後的文章。html
本文涉及知識點:前端
prototype
__proto__
new
call()
/apply()
/bind()
this
在本文中,jsliang 會講解經過自我探索後關於上述知識點的我的理解,若有紕漏、疏忽或者誤解,歡迎各位小夥伴留言指出。java
若是小夥伴對文章存有疑問,想快速獲得回覆。
或者小夥伴對 jsliang 我的的前端文檔庫感興趣,也想將本身的前端知識整理出來。
歡迎加 QQ 羣一塊兒探討:798961601
。git
不折騰的前端,和鹹魚有什麼區別github
目錄 |
---|
一 目錄 |
二 前言 |
三 題目 |
四 解題 |
五 知識拓展 |
5.1 問題少年:旅途開始 |
5.2 原型及原型鏈 |
5.3 new 爲什麼物 |
5.4 call() 又是啥 |
5.5 this 指向哪 |
六 總結 |
七 參考文獻 |
八 工具 |
返回目錄面試
廣州小夥伴在幫我進行面試摸底的時候,提出了問題:可否談談 this 的做用?編程
題目的目的:數組
call()
、apply()
、=>
)可是,我發現了我走了一條不歸路,無心間我看了下 prototype
!
而後,我爬上了一座高山……
相信有的小夥伴能自信地作出下面這些題~
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);
複製代碼
請寫出上面編程的輸出結果是什麼?
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();
複製代碼
請寫出上面編程的輸出結果是什麼?
function Person(name) {
this.name = name
}
let p = new Person('Tom');
複製代碼
問題1:1. p.__proto__等於什麼?
問題2:Person.__proto__等於什麼?
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);
複製代碼
請寫出上面編程的輸出結果是什麼?
b.n -> 1
b.m -> undefined;
c.n -> 2;
c.m -> 3;
複製代碼
f.a() -> a
f.b() -> f.b is not a function F.a() -> a F.b() -> b 複製代碼
答案1:Person.prototype
答案2:Function.prototype
foo.a => value a
foo.b => undefined
F.a => value a
F.b => value b
複製代碼
若是小夥伴們查看完答案,仍不知道怎麼回事,那麼,咱們擴展下本身的知識點,暢快了解更多地知識吧!
原型和原型鏈估計是老生常談的話題了,可是仍是有不少小白(例如 jsliang 本身)就時常懵逼在這裏。
首圖祭祖,讓暴風雨來得更猛烈些吧!
由於愛(瞭解前因後果),因此 jsliang 開始學習(百度)之旅,瞭解原型和原型鏈。
首先,jsliang 去了解查看原型鏈 prototype
。
而後,在瞭解途中看到了 new
,因而百度查看 JS 的 new
理念。
接着,接觸 new
會了解還有 call()
,而 call()
、apply()
以及箭頭函數 =>
又是類似的東西。
最後,當咱們查找 call()
的時候,它又涉及到了 this
,因此咱們 「順便」 查閱 this
吧。
首先,爲何須要原型及原型鏈?
咱們查看一個例子:
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 世界萬物誕生記》,感興趣的小夥伴能夠去了解一下。這裏咱們仍是看圖,並回歸本話題:
__proto__
產生了 No1 這號神,即:No1.__proto__ == null
。prototype
建立了對象 Object
,即:Object.prototype == No1; No1.__proto__ == null
。因而咱們把 prototype
叫作原型,就比如 Object
的原型是神,男人的原型是人類同樣,同時 __proto__
叫作原型鏈,畢竟有了 __proto__
,對象、神、JS 之間纔有聯繫。這時候 Object.prototype.__proto__ == null
。__proto__
借你用了。因此神根據 Object
,使用 __proto__
作了個機器 No2,即 No2.__proto__ == No1
,並規定全部的東西,經過 __proto__
能夠鏈接機器,再找到本身,包括 Object
也是,因而 Object 成爲全部對象的原型,Object.__proto__.__proto__ == No1
,而後 String
、Number
、Boolean
、 Array
這些物種也是如此。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;
複製代碼
這時候,咱們知道 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 了嗎?!!!
在點 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()
能作啥吧~
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()
方法調用父構造函數。
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()
方法調用匿名函數。
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
以及箭頭函數(=>
),因此下面咱們來了解了解吧~
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 來篇文章已被刷選。
jsliang 廣告推送:
也許小夥伴想了解下雲服務器
或者小夥伴想買一臺雲服務器
或者小夥伴須要續費雲服務器
歡迎點擊 雲服務器推廣 查看!
jsliang 的文檔庫 由 梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的做品創做。
本許可協議受權以外的使用權限能夠從 creativecommons.org/licenses/by… 處得到。