王福朋 - 博客園 —— 《 深入理解javascript原型和閉包》
1. 一切都是對象
本文要點1
一切(引用類型)都是對象,對象是屬性的集合。
1. javascript 數據類型
function show(x) {
console.log(typeof x); // undefined
console.log(typeof 10); // number
console.log(typeof 'abc'); // string
console.log(typeof true); // boolean
console.log(typeof function () {}); //function
console.log(typeof [1, 'a', true]); //object
console.log(typeof { a: 10, b: 20 }); //object
console.log(typeof null); //object
console.log(typeof new Number(10)); //object
}
show();
值類型(不是對象):undefined, number, string, boolean
引用類型(是對象):函數,數組,對象,null, new Number(10)
值類型和引用類型的類型判斷方式:
- 值類型的類型判斷用 typeof
- 引用類型的類型判斷使用 instanceof
var fn = function () {};
console.log(fn instanceof Object); // true
2. javascript 對象
定義:若干屬性的集合(只有屬性,沒有方法,方法也是一種屬性)
var obj = {
a: 10,
b: function (x) {
alert(this.a + x)
},
c: {
name: 'tanya',
year: 1975
}
}
函數也是一種對象(可以定義屬性):
var fn = function () {
alert(100);
};
fn.a = 10;
fn.b = function () {
alert(123);
};
fn.c = {
name: 'tanya',
year: 1975
}
數組也是一種對象(本身就存在 length
屬性):
var arr = [1, 2, 3];
console.log(arr.length); // 3
arr.a = 'tanya';
arr.b = function () {
alert(123);
}
arr.c = {
name: 'tanya',
year: 1975
}
for (var item in arr) {
console.log(item) // 0 1 a b c
}
2. 函數和對象的關系
本文要點2
對象是通過函數創建的,而函數又是一種對象。
對象是通過函數創建的
function Fn () {
this.name = 'tanya';
this.age = 1975;
}
var fn = new Fn(); // 說 fn 是對象,因為它有屬性
console.log(fn.name) // tanya
console.log(fn.age) // 1975
通過字面量創建對象或數組:
var obj = { a: 10, b: 20 };
var arr = [5, 'x', true];
等效于
var obj = new Object();
obj.a = 10;
obj.b = 20;
var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;
其中 Object 和 Array 是函數:
console.log(typeof Object); // function
console.log(typeof Array); // function
3. prototype 原型
什么是原型?
函數都有一個 prototype 屬性,它是一個對象,
函數的實例對象的 __proto__
屬性指向該函數的 prototype 屬性,
那我們稱該對象為函數實例對象的原型
本文要點3
理解原型概念。
1. prototype 和 constructor 屬性
函數有一個默認屬性 prototype
,屬性值是一個對象,該對象默認只有一個 constructor
屬性,指向這個函數本身。
如上圖:SuperType 是一個函數,右側的方框就是它的原型。
Object 原型里面有幾個其他屬性:
2. 在 prototype 上添加屬性
function Fn() {}
Fn.prototype.name = 'tanya'
Fn.prototype.getYear = function () {
return 1975;
};
4. 隱式原型
每個函數都有一個 prototype
屬性,即原型。每個對象都有一個 __proto__
,可稱為隱式原型。
本文要點4
理解隱式原型 __proto__
。
1. __proto__
屬性
關于 __proto__
屬性說明:是一個隱藏屬性,低版本瀏覽器不支持該屬性。
var obj = {};
console.log(obj.__proto__);
console.log(Object.prototype);
console.log(obj.__proto__ === Object.prototype);
以上代碼說明:每個對象都有一個 __proto__
屬性,指向創建該對象(這里是 obj)的函數(這里是 Object)的 prototype。
2. Object.prototype
的 __proto__
自定義函數的 prototype:本質和 var obj = {}
是一樣的,都是被 Object
創建的,所以它的 __proto__
指向的就是 Object.prototype
。
Object.prototype
是一個特例——它的 __proto__
指向的是 null。
3. 函數的 __proto__
函數也是一種對象,所以函數也有 __proto__
屬性。至于函數的 __proto__
是什么,還是要看函數是被誰創建的。
function fn(x, y) {
return x + y;
}
console.log(fn(10, 20)); // 30
// 等價于
var fn = new Function('x', 'y', 'return x + y;');
console.log(fn(5, 6)); // 11
函數是被 Function
創建的。
上面有說過:對象的 __proto__
指向的是創建它的函數的 prototype
,則會有 Object.__proto__ === Function.prototype
。
解釋一下:這里 Object 是一個函數,被 Function 所創建,函數也是一種對象,所以 Object 有 __proto__
屬性,指向創建它(Object)的函數(Function)的原型
上圖中,自定義函數的 Foo.__proto__
指向 Function.prototype
,Object.__proto__
指向 Function.prototype
,還有一個Function.__proto__
指向 Function.prototype
。
如何理解 Function.__proto__
指向 Function.prototype
?
Function
是一個函數,函數是一種對象,對象有 __proto__
屬性。 既然是函數,那么是被 Function
創建的,也就是 Function
是被自身創建的。對象的 __proto__
指向創建它的函數的 prototype
,所以 Function
的 __proto__
指向了自身的 prototype
。
5. instanceof
本文要點5
instanceof 判定規則。
instanceof 判定
語法是 A instanceof B
,A是對象,B一般是一個函數。
instanceof 判斷規則:沿著 A 的 __proto__
這條線來找,如果能找到與 B.prototype
相等的同一個引用,即同一個對象,就返回 true。如果找到終點(Object.prototype.__proto__
)還未重合,則返回 false。
代碼實現:
function instance_of(L, R) { // L 表示左表達式,R 表示右表達式
var O = R.prototype; // 取 R 的顯示原型
L = L.__proto__; // 取 L 的隱式原型
while (true) {
if (L === null){
return false;
}
if (O === L) { // 這里重點:當 O 嚴格等于 L 時,返回 true
return true;
}
L = L.__proto__;
}
}
根據上圖及 instanceof 判定規則理解:
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function instanceof Function); // true
6. “繼承”
本文要點6
- 理解原型鏈概念
- 知道屬性查找方式
1. 原型鏈
javascript 中的繼承是通過原型鏈來體現的。
當訪問一個對象的屬性時,先在基本屬性(自身屬性)中查找,如果沒有,再沿著 __proto__
這條鏈向上找,直到 Object.prototype.__proto__
,如果還沒找到就返回 undefined,這條在原型上查找的鏈稱為原型鏈。
function Foo() {}
var f1 = new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
console.log(f1.a); // 10
console.log(f1.b); // 200
2. hasOwnProperty
通過 hasOwnProperty 方法可以判斷屬性是基本的(自身)還是從原型中找到的。
function Foo() {}
var f1 = new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
for (var item1 in f1) {
if (f1.hasOwnProperty(item1)) { // 只打印自身屬性
console.log(item1); // a
}
}
for (var item2 in f1) {
console.log(item2); // a b
}
那么 hasOwnProperty 有是在哪來的呢?它是在 Objec.prototype 中定義的。
對象的原型鏈是沿著 __proto__
這條線走的,因此在查找 f1.hasOwnProperty
屬性時,就會順著原型鏈一直查找到 Object.prototype。
每個函數都有 call, bind 方法,都有 length, arguments, caller 等屬性。這也是“繼承”的。函數是由 Function 函數創建,__proto__
屬性指向 Function.prototype,因此繼承 Function.prototype 中的方法。
7. 原型靈活性
偶不想表。
8. 簡述【執行上下文】上
本文要點8
理解全局執行上下文環境。
全局執行上下文環境
在 javascript 代碼執行之前,瀏覽器會做一些“準備工作”,其中包括對變量的聲明,而不是賦值。變量賦值是在賦值語句執行的時候進行的。
- 變量、函數表達式——變量聲明,默認賦值為undefined;
- this——賦值;
- 函數聲明——賦值;
這三種數據的準備情況我們稱之為“執行上下文”或者“執行上下文環境”。
其實,javascript 在執行一個“代碼段”之前,都會進行這些“準備工作”來生成執行上下文。這個“代碼段”分為三種情況——全局代碼、函數體、eval代碼。
為什么“代碼段”分為這三種?
代碼段就是一段文本形式的代碼。首先,全局代碼是一種,本來就是手寫文本到 <script>
標簽里面的。
<script>
// 代碼段
</script>
其次,eval 代碼接受的也是一段文本形式的代碼。
eval('alert(123)')
最后,函數體是因為函數在創建時,本質上是 new Function(...)
得到的,其中需要傳入一個文本形式的參數作為函數體。
function fn(x) {
console.log(x + 5);
}
var fn = new Function('x', 'console.log(x + 5)');
9. 簡述【執行上下文】下
本文要點9
- 理解執行上下文環境
- 知道什么是自由變量
- 上下文的準備工作及區別(全局與函數)
1. 函數體執行上下文環境
function fn(x) {
console.log(arguments); // [10]
console.log(x); // 10
}
fn(10);
在函數體的語句執行之前,arguments 變量和函數參數都已經被賦值。函數每調用一次都會產生一個新的執行上下文環境,因為不同的調用可能會有不同的參數。
2. 自由變量
自由變量:當前作用域內使用了外部作用域的變量(使用了不是在當前作用域內定義的變量)。
函數在定義的時候(不是調用的時候),就已經確定了函數體內部自由變量的作用域。
var a = 10;
function fn() {
console.log(a); // a 是自由變量,函數創建時,就確定了 a 要取值的作用域
}
function bar(f) {
var a = 20;
f(); // 打印 10, 而不是 20
}
bar(fn);
3. 上下文環境
全局代碼的上下文環境數據內容為:
準備內容 | 初始化 |
---|---|
普通變量(包括函數表達式),如: var a = 10; | 聲明(默認賦值為 undefined) |
函數聲明, 如: function fn() { } | 賦值 |
this | 賦值 |
如果代碼段是函數體,那么需要在此(全局準備內容)基礎上附加:
準備內容 | 初始化 |
---|---|
參數 | 賦值 |
arguments | 賦值 |
自由變量的取值作用域 | 賦值 |
通俗執行上下文環境定義:在執行代碼之前,把將要用到的所有的變量都事先拿出來,有的直接賦值了,有的先用 undefined 占個空。
10. this
本文要點10
- 掌握 this 的幾種指向問題。
- 函數中 this 取值是在函數被調用的時候確定的,而不是在定義時。
1. 構造函數
所謂構造函數就是用來 new 對象的函數。嚴格來說,所有函數都可以 new 一個對象,但是有些函數的定義不是為了作為構造函數。另外注意,構造函數的函數名第一個字母大寫(規則約定)。例如:Object, Array, Function 等。
function Foo() {
this.name = 'tanya';
this.year = 1975;
console.log(this); // Foo { name: 'tanya', year: 1975 }
}
var f1 = new Foo();
console.log(f1.name); // tanya
console.log(f1.year); // 1975
以上代碼中,如果函數作為構造函數用,那么其中 this 就代表它即將 new 出來的對象, 這里 this 表示 f1。
2. 函數作為對象的一個屬性
如果函數作為對象的一個屬性時,并且作為對象的一個屬性被調用時,函數中的 this 指向該對象。
var obj = {
x: 10,
fn: function() {
console.log(this); // Object { x: 10, fn: function }
console.log(this.x); // 10
}
}
obj.fn();
如果 fn 函數不是作為 obj 的一個屬性被調用,會是什么結果?
var obj = {
x: 10,
fn: function() {
console.log(this); // Window {top: Window, ...}
console.log(this.x); // undefined
}
}
var fn1 = obj.fn;
fn1();
如上代碼,如果 fn 函數被賦值到了另一個變量中,并沒有作為 obj 的一個屬性被調用,那么 this 的值就是 window,this.x 為 undefined。
3. 函數用 call 或者 apply 調用
當一個函數被 call 或 apply 調用時,this 的值就取傳入的對象的值。
var obj = {
x: 10
};
var fn = function() {
console.log(this); // Object {x: 10}
console.log(this.x);
}
fn.call(obj);
4. 全局 & 普通函數調用(直接調用)
在全局環境下,this 永遠是 window:
console.log(this === window); // true
普通函數在調用時,其中 this 也是 window:
var x = 10;
var fn = function() {
console.log(this); // Window {top: Window ...}
console.log(this.x); // 10
}
fn();
注意下面的情況:
var obj = {
x: 10,
fn: function() {
function f() {
console.log(this); // Window {top: Window ...}
console.log(this.x); // 10
}
f();
}
};
obj.fn();
雖然函數 f 是在 obj.fn
內部定義的,但它仍然是一個普通函數,this 指向 window。
5. bind() 對直接調用的影響(新增)
Function.prototype.bind()
的作用是將當前函數與指定對象綁定,并返回一個新函數,這個函數無論以什么樣的方式調用,其 this 始終指向綁定的對象。
var obj = {};
function test() {
console.log(this === obj);
}
var testObj = test.bind(obj);
test(); // false
testObj(); // true
6. 箭頭函數中的 this(新增)
箭頭函數沒有自己的 this 綁定。箭頭函數中使用的 this,指的是直接包含它的那個函數或函數表達式中的 this。
var obj = {
test: function () {
var arrow = () => {
console.log(this === obj);
}
arrow();
}
}
obj.test(); // true
// 這里 arrow 函數中的 this 指的是 test 函數中的 this,
// 而 test 函數中的 this 指的是 obj
另外需要注意的是,箭頭函數不能用 new 調用,不能 bind() 到某個對象(雖然 bind() 方法調用沒問題,但是不會產生預期效果)。不管在什么情況下使用箭頭函數,它本身是沒有綁定 this 的,它用的是直接外層函數(即包含它的最近的一層函數或函數表達式)綁定的 this。
11. 執行上下文棧
本文要點11
- 理解執行上下文棧概念
- 了解壓棧、出棧過程。
1. 執行上下文棧概念
執行全局代碼時,會產生一個執行上下文環境,每次調用函數都又會產生執行上下文環境。當函數調用完成時,這個上下文環境以及其中的數據都會被清除,再重新回到全局上下文環境。處于活動狀態的執行上下文環境只有一個。
可以把這看成是一個壓棧出棧的過程,俗稱執行上下文棧。
藍色背景表示活動狀態
白色背景表示非活動狀態
2. 壓棧、出棧過程
var a = 10, // 1、進入全局上下文環境
fn,
bar = function(x) {
var b = 5;
fn(x+b);// 用 B 表示 // 3、進入 fn 函數上下文環境
}
fn = function(y) {
var c = 5;
console.log(y + c);
}
// 用 A 表示
bar(10); // 2、進入 bar 函數上下文環境
第一步:在代碼執行之前,首先創建全局上下文環境:
全局上下文環境(全局代碼執行前)
變量 | 賦值 |
---|---|
a | undefined |
fn | undefined |
bar | undefined |
this | window |
然后是代碼執行,執行到 A 之前,全局上下文環境中的變量都在執行過程中被賦值:
全局上下文環境變為
變量 | 賦值 |
---|---|
a | 10 |
fn | function |
bar | function |
this | window |
第二步:執行到 A 之后,調用 bar 函數。
跳轉到 bar 函數內部,執行函數體語句之前,會創建一個新的執行上下文環境:
bar 函數執行上下文環境
變量 | 賦值 |
---|---|
b | undefined |
x | 10 |
arguments | [10]] |
this | window |
并將這個執行上下文環境壓棧,設置為活動狀態:
第三步:執行到 B,又調用了 fn 函數,在執行函數體語句之前,會創建 fn 函數的執行上下文環境,并壓棧,設置為活動狀態:
第四步:待 B 執行完畢,即 fn 函數執行完畢后,此次調用 fn 所生成的上下文環境出棧,并且被銷毀(已經用完了,就要及時銷毀,釋放內存)。
第五步:同理,待 A 執行完畢,即 bar 函數執行完畢后,調用 bar 函數所生成的上下文環境出棧,并且被銷毀(已經用完了,就要及時銷毀,釋放內存)。
12. 簡介【作用域】
本文要點12
理解作用域。
作用域
作用域是個很抽象的概念,類似于一個“地盤”。
上圖中,全局代碼和 fn 、bar 兩個函數都會形成一個作用域。
在作用域中存在上下級關系,上下級關系的確定就看函數是在哪個作用域下創建的。例如:fn 作用域下創建了 bar 函數,那么 “fn 作用域” 就是 “bar 作用域” 的上級。
作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。例如以上代碼中,三個作用域下都聲明了“a” 這個變量,但是他們不會有沖突。各自作用域下,用各自的“a”。
13. 【作用域】和【上下文環境】
本文要點13
理解作用域和上下文環境。
作用域結合上下文環境
除了全局作用域外,每個函數都會創建自己的作用域,作用域在函數定義時就已經確定了,而不是在函數調用時確定的。
下面按照程序執行的順序,一步一步加上上下文環境:
第一步,在加載程序時,就已經確定了全局上下文環境,并隨著程序的執行而對變量賦值:
第二步,程序執行到 A ,調用 fn(10) ,此時生成此次調用 fn 函數時的上下文環境,壓棧,并將此上下文環境設置為活動狀態。
第三步,執行到 B 時,調用 bar(100) ,生成此次調用的上下文環境,壓棧,并設置為活動狀態。
第四步,執行完 B ,bar(100) 調用完成。則 bar(100) 上下文環境被銷毀。接著執行 C,調用 bar(200),則又生成 bar(200 )的上下文環境,壓棧,設置為活動狀態。
第五步,執行完 C ,則 bar(200) 調用結束,其上下文環境被銷毀。此時會回到 fn(10) 上下文環境,變為活動狀態。
第六步,執行完 A,fn(10) 執行完成之后,fn(10) 上下文環境被銷毀,全局上下文環境又回到活動狀態。
最后把以上過程連起來看看:
作用域只是一個“地盤”,一個抽象的概念,其中沒有變量,要通過作用域對應的執行上下文環境來獲取變量的值。同一個作用域下,不同的調用會產生不同的執行上下文環境,繼而產生不同變量的值。所以,作用域中變量的值是在執行過程中產生的確定的,而作用域卻是在函數創建時就確定了。
所以,如果要找一個作用域下某個變量的值,就需要找到這個作用域對應的執行上下文環境,再在其中尋找變量的值。
14. 從【自由變量】到【作用域鏈】
本文要點14
理解作用域鏈。
1. 自由變量
什么是自由變量?
在 A 作用域中使用變量 x,卻沒有在 A 作用域中聲明(在其他作用域中聲明的),對于 A 作用域來說,x就是一個自由變量。
如下:
var x = 10;
function fn() {
var b = 20;
console.log(x + b); // x 在這里就是一個自由變量
}
那么,在 fn 函數中,取自由變量 x 的值時,要到哪個作用域中去???——要到創建 fn 函數的那個作用域中取。無論 fn 函數在哪里調用。
2. 作用域鏈
作用域鏈:在當前作用域中進行查找,如果沒有,就在創建函數作用域中查找自由變量,如果沒有,就去創建該作用域的函數所在作用域查找,直到全局作用域為止。這個在作用域中查找的路線,稱之為作用域鏈。
我們拿文字總結一下取自由變量時的這個“作用域鏈”過程:(假設a是自由量)
第一步,現在當前作用域查找a,如果有則獲取并結束。如果沒有則繼續;
第二步,如果當前作用域是全局作用域,則證明a未定義,結束;否則繼續;
第三步,(不是全局作用域,那就是函數作用域)將創建該函數的作用域作為當前作用域;
第四步,跳轉到第一步。
示例代碼:
以上代碼中:fn() 返回的是 bar 函數 ,賦值給 x 。執行 x(),即執行 bar 函數代碼。取 b 的值時,直接在 fn 作用域取出。取 a 的值時,試圖在 fn 作用域取,但是取不到,只能轉向創建 fn 的那個作用域中去查找,結果找到了。
15. 閉包
原文鏈接:http://www.cnblogs.com/wangfupeng1988/p/3992795.html
參考:https://stackoverflow.com/questions/111102/how-do-javascript-closures-work
本文要點15
- 知道閉包產生的條件及常見用法
- 理解閉包是什么
1. 閉包產生的條件
- 函數嵌套
- 內部函數引用了外部函數的數據(可以是變量或者函數)
換句話說:簡單地訪問函數的詞法作用域(靜態作用域)以外的自由變量會創建一個閉包。
function fn () { // 外部函數
var max = 10;
function bar (x) { // 內部函數
if (x > max) { // 引用了外部函數變量 max
console.log(x)
}
}
}
fn()
說明:紅色框中,收起部分為 object: { max: undefined }
那么,閉包到底是什么?
閉包是包含被內部函數引用的在外部函數中定義的數據的對象(可以是變量或者函數)。簡單點說是:包含被引用變量(函數)的對象
2. 常見的閉包
- 將函數作為另一個函數的返回值(函數不必為了被稱為閉包而返回,看看閉包產生的條件)
- 將函數作為實參傳遞給另一個函數調用
將函數作為另一個函數的返回值
function fn () {
var max = 10;
function bar () {
return max++;
}
return bar; // 作為另一個函數的返回值
}
var bar = fn();
console.log(bar()); // 10
console.log(bar()); // 11
console.log(bar()); // 12
從中我們可以看出閉包的作用:
- 使函數內部的變量
max
在函數fn
執行完后,讓然存活在內存中(其他變量已經釋放) - 讓函數
fn
外部可以讀取到函數內部的數據max
(變量或者函數)
也可以看出閉包的缺點:
內存泄漏(Memory Leak)是指程序中己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。
——百度百科
- 函數
fn
執行完之后,被函數bar
引用的變量沒有釋放,占用內存時間會變長 - 可能造成內存泄漏(memory leaks)
那對應的解決方案是:
只需要在不使用函數時,置空就行了 bar = null;
將函數作為實參傳遞給另一個函數調用
function fn () {
var max = 1;
var interval;
interval = setInterval(function () { // 將匿名函數作為 setInterval 函數的參數調用
if (max > 100) clearInterval(interval)
console.log(max++);
}, 1000)
}
fn();
注意圖中 Closure (fn)
,fn 指的是查找引用變量時的作用域。
3. 閉包作用域
代碼一
function fn () { // Closure (fn) { x: 1 }
var x = 1;
function foo () { // Closure (foo) { y: 2 }
var y = 2;
console.log(x + y)
return function bar () {
var z = 3;
console.log(x + y + z)
}
}
}
var foo = fn();
var bar = foo();
bar();
從上圖中可以看到,閉包所在作用域平行于引用變量(x, y)所在的作用域。
代碼二(繼續說明閉包所在作用域)
function fn () {
var x = 1;
function foo () {
console.log(x++);
}
function bar () {
console.log(x++);
}
return {
foo: foo,
bar: bar
}
}
var o = fn();
var foo = o.foo;
var bar = o.bar;
foo(); // x: 1
bar(); // x: 2
從代碼輸出可以看到,閉包并不是屬于某一個內部函數,也恰好印證了上面說的。
16. 總結
文章說明
感謝王福朋
內容絕大部分來自王福朋 - 博客園 —— 《 深入理解javascript原型和閉包
》,我只是重畫了大部分的圖和對少量內容進行了補充(比如:this 章節的“新增”、閉包部分)
文章初衷
希望在一篇文章中進行概括說明這些內容(并不是說分開不好),只是這樣讀起來更加順暢,找起來比較方便(離線也可以哦,已上傳到 Github上,歡迎 star、fork);
自己也可以針對相關內容進行擴充(不用看到一些額外的相關知識就收藏一個網址_,你也可以把這篇文章轉到你的賬號下,進行擴充,說明來源即可);
文章反饋
如有錯誤,歡迎指出;
如有疑問,歡迎討論;
文章后續
如果看到相關的新的知識,會添加到對應主題下面(以“新增”標明);
如果你看到這里沒有提到的相關內容,也可以給我鏈接,我會進行補充,并貼上你的大名;
撒花,待續,期待你們的加入……