深入淺出JavaScript面向對象編程

0 寫在前面的話

大多數的面向對象編程語言中,比如C++和Java,在使用他們完成任務之前,必須創建類(class)。但在JavaScript中并不需要或者說不強制使用類。
面向對象都有如下幾種特性:

  1. 封裝
    數據可以和數據操作的功能組織在一起
  2. 聚合
    一個對象可以引用另一個對象
  3. 繼承
    一個新創建的對象和另一個對象擁有同樣的特性,而無需顯示復制功能
  4. 多態
    一個接口可以被多個對象實現

1 一切都是對象

“一切都是對象”這句話的重點在于如何去理解“對象”這個概念。
——當然,也不是所有的都是對象,值類型就不是對象。

首先咱們還是先看看javascript中一個常用的運算符——typeof。typeof應該算是咱們的老朋友,還有誰沒用過它?

typeof函數輸出的一共有幾種類型,在此列出:
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

以上代碼列出了typeof輸出的集中類型標識,其中上面的四種(undefined, number, string, boolean)屬于簡單的值類型,不是對象。剩下的幾種情況——函數、數組、對象、null、new Number(10)都是對象。他們都是引用類型,也就是我們說的對象。

1.1 對象的創建

ES5里面有兩種方法可以創建對象,或者說實例化對象

  1. new構造函數
  2. 對象字面量
    在ES6當中, 允許直接寫入變量和函數,作為對象的屬性和方法。這樣的書寫更加簡潔。
var birth = '2000/01/01';
var Person = {
  name: '張三',
  //等同于birth: birth
  birth,
  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }
};

1.2 函數和對象的關系

函數是一種對象,但是函數卻不像數組一樣——你可以說數組是對象的一種,因為數組就像是對象的一個子集一樣。但是函數與對象之間,卻不僅僅是一種包含和被包含的關系,函數和對象之間的關系比較復雜,甚至有一點雞生蛋蛋生雞的邏輯。

        function Fn() {
            this.name = '張三';
            this.year = 1988;
        }
        var fn1 = new Fn();

這個例子很簡單,它能說明:對象可以通過函數來創建。對!也只能說明這一點。
但是我要說——對象都是通過函數創建的——有些人可能反駁:不對!因為:

var obj = { a: 10, b: 20 };

其實以上代碼的本質是:

var obj = new Object();
        obj.a = 10;
        obj.b = 20;

所以,可以很負責任的說——對象都是通過函數來創建的。現在是不是糊涂了—— 對象是函數創建的,而函數卻又是一種對象。天哪!函數和對象到底是什么關系啊?

2 原型對象

我們可以把原型對象看作是對象的基類。幾乎所有的函數都有一個名為prototype的屬性,該屬性是一個原型對象用來創建新的對象實例。所有創建的對象實例共享該原型對象。

2.1 prototype屬性

這個prototype的屬性值是一個對象(屬性的集合,再次強調!),默認的只有一個叫做constructor的屬性,指向這個函數本身。


image.png

原型既然作為對象,屬性的集合,不可能就只弄個constructor來玩玩,肯定可以自定義的增加許多屬性。例如這位Object大哥,人家的prototype里面,就有好幾個其他屬性。


image.png

一個對象實例通過內部屬性[[prototype]]追蹤原型對象。該屬性是一個指向該實例使用的原型對象的指針,當new一個新的對象時,構造函數的原型對象就會賦給該對象的prototype屬性。
 function Fn() { }
        Fn.prototype.name = '張三';
        Fn.prototype.getYear = function () {
            return 1988;
        };

        var fn = new Fn();
        console.log(fn.name);
        console.log(fn.getYear());

Fn是一個函數,fn對象是從Fn函數new出來的,這樣fn對象就可以調用Fn.prototype中的屬性。

因為每個對象都有一個隱藏的屬性——“proto”,這個屬性引用了創建這個對象的函數的prototype。即:fn.proto === Fn.prototype

2.2 隱式原型 __proto__

每個函數function都有一個prototype,即原型。這里再加一句話——每個對象都有一個proto,可成為隱式原型。
該屬性沒有寫入 ES6 的正文,而是寫入了附錄,原因是__proto__前后的雙下劃線,說明它本質上是一個內部屬性,而不是一個正式的對外的 API,只是由于瀏覽器廣泛支持,才被加入了 ES6。標準明確規定,只有瀏覽器必須部署這個屬性,其他運行環境不一定需要部署,而且新的代碼最好認為這個屬性是不存在的。因此,無論從語義的角度,還是從兼容性的角度,都不要使用這個屬性,而是使用下面的Object.setPrototypeOf()(寫操作)、Object.getPrototypeOf()(讀操作)、Object.create()(生成操作)代替。
在實現上,__proto__調用的是Object.prototype.__proto__

image.png

那么上圖中的“Object prototype”也是一個對象,它的proto指向哪里?

好問題!

在說明“Object prototype”之前,先說一下自定義函數的prototype。自定義函數的prototype本質上就是和 var obj = {} 是一樣的,都是被Object創建,所以它的proto指向的就是Object.prototype。
但是Object.prototype確實一個特例——它的__proto__指向的是null,切記切記!

image.png

對象的proto指向的是創建它的函數的prototype,就會出現:Object.proto === Function.prototype。用一個圖來表示。

image.png

上圖中,很明顯的標出了:自定義函數Foo.proto指向Function.prototype,Object.proto指向Function.prototype,唉,怎么還有一個……Function.proto指向Function.prototype?這不成了循環引用了?是的,它是一個環形結構。
其實稍微想一下就明白了。Function也是一個函數,函數是一種對象,也有__proto__屬性。既然是函數,那么它一定是被Function創建。所以——Function是被自身創建的。所以它的__proto__指向了自身的Prototype。

最后一個問題:Function.prototype指向的對象,它的__proto__是不是也指向Object.prototype

答案是肯定的。因為Function.prototype指向的對象也是一個普通的被Object創建的對象,所以也遵循基本的規則。

2.3 instanceof

Instanceof運算符的第一個變量是一個對象,暫時稱為A;第二個變量一般是一個函數,暫時稱為B。

Instanceof的判斷隊則是:沿著A的proto這條線來找,同時沿著B的prototype這條線來找,如果兩條線能找到同一個引用,即同一個對象,那么就返回true。如果找到終點還未重合,則返回false。

image.png

問題又出來了。Instanceof這樣設計,到底有什么用?到底instanceof想表達什么呢?
重點就這樣被這位老朋友給引出來了——繼承——原型鏈。
即,instanceof表示的就是一種繼承關系,或者原型鏈的結構

3 繼承

繼承是JavaScript面向對象編程中非常重要的概念,js中不支持借口繼承,實現繼承主要依靠原型鏈來實現。

3.1原型鏈

原型鏈是什么我們已經在上文中詳細講過了。原型鏈繼承基本思想就是讓一個原型對象指向另一個類型的實例。

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
  return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true

代碼定義了兩個類型SuperType和SubType,每個類型分別有一個屬性和一個方法,SubType繼承了SuperType,而繼承是通過創建SuperType的實例,并將該實例賦給SubType.prototype實現的。

實現的本質是重寫原型對象,代之以一個新類型的實例,那么存在SuperType的實例中的所有屬性和方法,現在也存在于SubType.prototype中了。
我們知道,在創建一個實例的時候,實例對象中會有一個內部指針指向創建它的原型,進行關聯起來,在這里代碼SubType.prototype = new SuperType(),也會在SubType.prototype創建一個內部指針,將SubType.prototype與SuperType關聯起來。

所以instance指向SubType的原型,SubType的原型又指向SuperType的原型,繼而在instance在調用getSuperValue()方法的時候,會順著這條鏈一直往上找。

添加方法

在給SubType原型添加方法的時候,如果,父類上也有同樣的名字,SubType將會覆蓋這個方法,達到重新的目的。 但是這個方法依然存在于父類中。

注意
記住不能以字面量的形式添加,因為,上面說過通過實例繼承本質上就是重寫,再使用字面量形式,又是一次重寫了,但這次重寫沒有跟父類有任何關聯,所以就會導致原型鏈截斷。

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype = {
  getSubValue:function () {
   return this.subproperty
  }
}
var instance = new SubType()
console.log(instance.getSuperValue())  // error

缺點
單純的使用原型鏈繼承,主要問題來自包含引用類型值的原型。在構造函數定義一個屬性后,通過原型繼承這個屬性就會出現在繼承的prototype中,繼承后的所有實例都會共享這個屬性


3.2借用構造函數

此方法為了解決原型中包含引用類型值所帶來的問題。

這種方法的思想就是在子類構造函數的內部調用父類構造函數,可以借助apply()和call()方法來改變對象的執行上下文

function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {
  // 繼承SuperType
  SuperType.call(this)
}
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors)  // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green"]

傳遞參數

借助構造函數還有一個優勢就是可以傳遞參數

function SuperType(name) {
  this.name = name
}
function SubType() {
  // 繼承SuperType
  SuperType.call(this, 'Jiang')
 
  this.job = 'student'
}
var instance = new SubType()
console.log(instance.name)  // Jiang
console.log(instance.job)   // student

如果僅僅借助構造函數,方法都在構造函數中定義,因此函數無法達到復用

3.3 組合繼承(原型鏈+構造函數)

組合繼承是將原型鏈繼承和構造函數結合起來,從而發揮二者之長的一種模式。

思路就是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。

這樣,既通過在原型上定義方法實現了函數復用,又能夠保證每個實例都有它自己的屬性。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)

  this.job = job
}
// 繼承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SuperType
SubType.prototype.sayJob = function() {
  console.log(this.job)
}
var instance1 = new SubType('Jiang', 'student')
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() // 'Jiang'
instance1.sayJob()  // 'student'
var instance2 = new SubType('J', 'doctor')
console.log(instance2.colors) // //["red", "blue", "green"]
instance2.sayName()  // 'J'
instance2.sayJob()  // 'doctor'

這種模式避免了原型鏈和構造函數繼承的缺陷,融合了他們的優點,是最常用的一種繼承模式。

3.4 原型式繼承

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

在object函數內部,先創建一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回這個臨時類型的一個新實例。

本質上來說,object對傳入其中的對象執行了一次淺復制。

var person = {
  name: 'Jiang',
  friends: ['Shelby', 'Court']
}
var anotherPerson = object(person)
console.log(anotherPerson.friends)  // ['Shelby', 'Court']

Object.create()方法

ES5通過Object.create()方法規范了原型式繼承,可以接受兩個參數,一個是用作新對象原型的對象和一個可選的為新對象定義額外屬性的對象,行為相同,基本用法和上面的object一樣,除了object不能接受第二個參數以外。

Object.create(proto, [ propertiesObject ])

proto
一個對象,應該是新創建的對象的原型。
propertiesObject
可選。該參數對象是一組屬性與值,該對象的屬性名稱將是新創建的對象的屬性名稱,值是屬性描述符(這些屬性描述符的結構與Object.defineProperties()
的第二個參數一樣)。注意:該參數對象不能是 undefined
,另外只有該對象中自身擁有的可枚舉的屬性才有效,也就是說該對象的原型鏈上屬性是無效的。

3.5寄生式繼承

寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用于封裝繼承過程的函數。

function createAnother(o) {
  var clone = Object.create(o) // 創建一個新對象
  clone.sayHi = function() { // 添加方法
    console.log('hi')
  }
  return clone  // 返回這個對象
}
var person = {
  name: 'Jiang'
}
var anotherPeson = createAnother(person)
anotherPeson.sayHi()

基于person返回了一個新對象anotherPeson,新對象不僅擁有了person的屬性和方法,還有自己的sayHi方法。

在主要考慮對象而不是自定義類型和構造函數的情況下,這是一個有用的模式。

3.7寄生組合式繼承

在前面說的組合模式(原型鏈+構造函數)中,繼承的時候需要調用兩次父類構造函數。

父類

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

第一次在子類構造函數中

function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)

  this.job = job
}

第二次將子類的原型指向父類的實例

// 繼承方法
SubType.prototype = new SuperType()

當使用var instance = new SubType()的時候,會產生兩組name和color屬性,一組在SubType實例上,一組在SubType原型上,只不過實例上的屏蔽了原型上的。

使用寄生式組合模式,可以規避這個問題。
這種模式通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。

基本思路:不必為了指定子類型的原型而調用父類的構造函數,我們需要的無非就是父類原型的一個副本。

本質上就是使用寄生式繼承來繼承父類的原型,在將結果指定給子類型的原型。

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}

該函數實現了寄生組合繼承的最簡單形式。

這個函數接受兩個參數,一個子類,一個父類。

第一步創建父類原型的副本,第二步將創建的副本添加constructor屬性,第三部將子類的原型指向這個副本。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)
 
  this.job = job
}
// 繼承
inheritPrototype(SubType, SuperType)
var instance = new SubType('Jiang', 'student')
instance.sayName()

補充:直接使用Object.create來實現,其實就是將上面封裝的函數拆開,這樣演示可以更容易理解。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)
 
  this.job = job
}
// 繼承
SubType.prototype = Object.create(SuperType.prototype)
// 修復constructor
SubType.prototype.constructor = SubType
var instance = new SubType('Jiang', 'student')
instance.sayName()

ES6新增了一個方法,Object.setPrototypeOf,可以直接創建關聯,而且不用手動添加constructor屬性。

3.8 ES6實現繼承

class, extends, super

這三個特性涉及了ES5中最令人頭疼的的幾個部分:原型、構造函數,繼承...你還在為它們復雜難懂的語法而煩惱嗎?你還在為指針到底指向哪里而糾結萬分嗎?

有了ES6我們不再煩惱!
ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念。新的class寫法讓對象原型的寫法更加清晰、更像面向對象編程的語法,也更加通俗易懂。

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        console.log(this.type + ' says ' + say)
    }
}

let animal = new Animal()
animal.says('hello') //animal says hello

class Cat extends Animal {
    constructor(){
        super()
        this.type = 'cat'
    }
}

let cat = new Cat()
cat.says('hello') //cat says hello

上面代碼首先用class定義了一個“類”,可以看到里面有一個constructor方法,這就是構造方法,而this關鍵字則代表實例對象。簡單地說,constructor內定義的方法和屬性是實例對象自己的,而constructor外定義的方法和屬性則是所有實例對象可以共享的

Class之間可以通過extends關鍵字實現繼承*,這比ES5的通過修改原型鏈實現繼承,要清晰和方便很多。上面定義了一個Cat類,該類通過extends
關鍵字,繼承了Animal類的所有屬性和方法。

super關鍵字,它指代父類的實例(即父類的this對象)。子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因為子類沒有自己的this對象,而是繼承父類的this對象,然后對其進行加工。如果不調用super方法,子類就得不到this對象。

ES6的繼承機制,實質是先創造父類的實例對象this(所以必須先調用super方法),然后再用子類的構造函數修改this。

P.S 如果你寫react的話,就會發現以上三個東西在最新版React中出現得很多。創建的每個component都是一個繼承React.Component
的類。詳見react文檔

4 執行上下文

在全局環境下的代碼段中,執行上下文環境中有如何數據:

  • 變量、函數表達式——變量聲明,默認賦值為undefined;
  • this——賦值;
  • 函數聲明——賦值

如果在函數中,除了以上數據之外,還會有其他數據。在函數體的語句執行之前,arguments變量和函數的參數都已經被賦值。函數每被調用一次,都會產生一個新的執行上下文環境。因為不同的調用可能就會有不同的參數。

另外一點不同在于,函數在定義的時候(不是調用的時候),就已經確定了函數體內部自由變量的作用域。
給執行上下文環境下一個通俗的定義——在執行代碼之前,把將要用到的所有的變量都事先拿出來,有的直接賦值了,有的先用undefined占個空。
講完了上下文環境,又來了新的問題——在執行js代碼時,會有數不清的函數調用次數,會產生許多個上下文環境。這么多上下文環境該如何管理,以及如何銷毀而釋放內存呢?下面將通過“執行上下文棧”來解釋這個問題。

完成一段js代碼

  • 引擎
  • 編譯器
  • 作用域
var a = 1;

分為兩步進行

  1. var a編譯器詢問當前作用域是否存在a,如果是,忽略該聲明;如果是,編譯器會在當前作用域創建一個新變量,并命名為a
  2. 編譯器為引擎生成運行時所需要的代碼。引擎運行時會詢問當前作用域,是否存在一個a變量,如果存在,就會繼續使用,如果不存在就會繼續尋找,如果找到就為它賦值

有關編輯器,編輯器在第二步生成代碼,引擎執行前,會通過查找a觀察他是否被聲明,會通過作用域查找他是否申明過,查詢類型有兩種,一種為LHS,一種為RHS.

LHS

賦值操作的左側進行查詢,查找的目的是為了給變量賦值。

RHS

賦值操作的右側進行查詢,查詢的目的是為了查詢變量的值

作用域的嵌套

lhs和rhs查詢都會在當前作用域開始查找,如果有需要,就回向上層作用域繼續查找,直到頂層。

異常

RHS失敗會拋出一個regerenceerror錯誤異常,不成功的LHS會創建一個全局變量(非嚴格模式下)。

4.1 詞法作用域

欺騙詞法

  • eval()
  • with 用作重復引用同一個對象中
    多個屬性
with (obj){
    a= 3;
    b= 4;
    }

區別:eval()是修改了所處的詞法作用域,而with()是創造了一個新的詞法作用域:本質是通過一個對象的引用當做作用域處理,將對象的屬性當做作用域中的標識符處理,創造了新的詞法作用域
缺點:無法在編譯時對作用域查找進行優化

(settimerout() ;setinterval();new function 等函數都可以接受字符串為參數,解釋為動態執行的代碼)

4.2 執行上下文

執行全局代碼時,會產生一個執行上下文環境,每次調用函數都又會產生執行上下文環境。當函數調用完成時,這個上下文環境以及其中的數據都會被消除,再重新回到全局上下文環境。處于活動狀態的執行上下文環境只有一個。


image.png

上代碼

var a = 10,        //全局上下文
              fn ,
              bar = function(x){
                     var b= 5;
                     fn(x+ b);    //執行fn,進入fn的上下文
              };

fn = function(y) {
        var c=5;
        console.log( y +c);
}

bar(10);    //bar的上下文

在執行代碼之前,首先將創建全局上下文環境。



然后是代碼執行。代碼執行到第12行之前,上下文環境中的變量都在執行過程中被賦值。



執行到第13行,調用bar函數。
跳轉到bar函數內部,執行函數體語句之前,會創建一個新的執行上下文環境。

并將這個執行上下文環境壓棧,設置為活動狀態。



執行到第5行,又調用了fn函數。進入fn函數,在執行函數體語句之前,會創建fn函數的執行上下文環境,并壓棧,設置為活動狀態。

待第5行執行完畢,即fn函數執行完畢后,此次調用fn所生成的上下文環境出棧,并且被銷毀(已經用完了,就要及時銷毀,釋放內存)。



同理,待第13行執行完畢,即bar函數執行完畢后,調用bar函數所生成的上下文環境出棧,并且被銷毀(已經用完了,就要及時銷毀,釋放內存)。


4.3函數作用域

作用域只是一個“地盤”,一個抽象的概念,其中沒有變量。要通過作用域對應的執行上下文環境來獲取變量的值。同一個作用域下,不同的調用會產生不同的執行上下文環境,繼而產生不同的變量的值。所以,作用域中變量的值是在執行過程中產生的確定的,而作用域卻是在函數創建時就確定了。

所以,如果要查找一個作用域下某個變量的值,就需要找到這個作用域對應的執行上下文環境,再在其中尋找變量的值。

切記是要到創建這個函數的那個作用域中取值——是“創建”,而不是“調用”,切記切記——其實這就是所謂的“靜態作用域”。

4.4 閉包

使用閉包主要是為了設計私有的方法和變量。閉包的優點是可以避免全局變量的污染,缺點是閉包會常駐內存,會增大內存使用量,使用不當很容易造成內存泄露。
閉包有三個特性:
1.函數嵌套函數 2.函數內部可以引用外部的參數和變量 3.參數和變量不會被垃圾回收機制回收

于“閉包”這個詞的概念的文字描述,確實不好解釋,我看過很多遍,但是現在還是記不住。
但是你只需要知道應用的兩種情況即可——函數作為返回值,函數作為參數傳遞。

  1. 函數作為返回值
  2. 函數作為參數被傳遞
function fn(){
    var max = 10;
    return function(x){
        if(x >max){
            console.log(x)
                }
    }
}
var f1 =fn();
f1(15)

第一步,代碼執行前生成全局上下文環境,并在執行時對其中的變量進行賦值。此時全局上下文環境是活動狀態。


第二步,執行第17行代碼時,調用fn(),產生fn()執行上下文環境,壓棧,并設置為活動狀態。


第三步,執行完第17行,fn()調用完成。按理說應該銷毀掉fn()的執行上下文環境,但是這里不能這么做。注意,重點來了:因為執行fn()時,返回的是一個函數。函數的特別之處在于可以創建一個獨立的作用域。而正巧合的是,返回的這個函數體中,還有一個自由變量max要引用fn作用域下的fn()上下文環境中的max。因此,這個max不能被銷毀,銷毀了之后bar函數中的max就找不到值了。
因此,這里的fn()上下文環境不能被銷毀,還依然存在與執行上下文棧中。
——即,執行到第18行時,全局上下文環境將變為活動狀態,但是fn()上下文環境依然會在執行上下文棧中。另外,執行完第18行,全局上下文環境中的max被賦值為100。如下圖:


第四步,執行到第20行,執行f1(15),即執行bar(15),創建bar(15)上下文環境,并將其設置為活動狀態。



執行bar(15)時,max是自由變量,需要向創建bar函數的作用域中查找,找到了max的值為10。這個過程在作用域鏈一節已經講過。
這里的重點就在于,創建bar函數是在執行fn()時創建的。fn()早就執行結束了,但是fn()執行上下文環境還存在與棧中,因此bar(15)時,max可以查找到。如果fn()上下文環境銷毀了,那么max就找不到了。
使用閉包會增加內容開銷,現在很明顯了吧!

第五步,執行完20行就是上下文環境的銷毀過程,這里就不再贅述了。

5 this

this的取值分四個情況
敲黑板:在函數中this到底取何值,是在函數真正被調用執行的時候確定的,函數定義的時候確定不了。因為this的取值是執行上下文環境的一部分,每次調用函數,都會產生一個新的執行上下文環境。

  1. 構造函數
    所謂構造函數就是用來new對象的函數。其實嚴格來說,所有的函數都可以new一個對象,但是有些函數的定義是為了new一個對象,而有些函數則不是。另外注意,構造函數的函數名第一個字母大寫(規則約定)。例如:Object、Array、Function等。
function Foo(){
    this.name="rxy";
    this.year="1996";
    console.log(this)     //Foo {name:"rxy",year:"1996"}
}
var fi =new Foo();   //實例化

以上代碼中,如果函數作為構造函數用,那么其中的this就代表它即將new出來的對象。
注意,以上僅限new Foo()的情況,即Foo函數作為構造函數的情況。如果直接調用Foo函數,而不是new Foo(),情況就大不一樣了。

function Foo(){
    this.name="rxy";
    this.year="1996";
    console.log(this)     //Window
}
Foo() //這種情況下this是window
  1. 函數作為對象的一個屬性
    如果函數作為對象的一個屬性時,并且作為對象的一個屬性被調用時,函數中的this指向該對象。
var obj = {
    x: 10,
    fn:function(){
        console.log(this)   //object {x:10,fn:function}
    }    
}
obj.fn

以上代碼中,fn不僅作為一個對象的一個屬性,而且的確是作為對象的一個屬性被調用。結果this就是obj對象。

注意,如果fn函數不作為obj的一個屬性被調用,會是什么結果呢?

var obj = {
    x: 10,
    fn:function(){
        console.log(this)   //object {x:10,fn:function}
    }    
}
var fn1= obj.fn
fn1()

如上代碼,如果fn函數被賦值到了另一個變量中,并沒有作為obj的一個屬性被調用,那么this的值就是window,this.x為undefined。

  1. 函數用call或者apply或者bind
    當一個函數被call或者apply或者bind調用時。this的值就傳入對象的值。

  2. 全局 & 調用普通函數
    在全局環境下,this永遠是window,這個應該沒有非議。

console.log(this === global);  //true

普通函數在調用時,其中的this也都是window。

var obj={
    x:10;
    fn:function(){
            function() f(){
                console.log(this);   // Window{...}
            }
    }
}

雖然f在obj內部定義的,但他任然是一個普通函數 this任然指向window
以上代碼是從jQuery中摘除來的部分代碼。jQuery.extend和jQuery.fn.extend都指向了同一個函數,但是當執行時,函數中的this是不一樣的。

執行jQuery.extend(…)時,this指向jQuery;執行jQuery.fn.extend(…)時,this指向jQuery.fn。

這樣就巧妙的將一段代碼同時共享給兩個功能使用,更加符合設計原則。

  1. 在構造函數的prototype中的this
function Fn(){
        this.name="rxy";
        this,age="20";
}
Fn.prototype.getName = function(){
        console.log(this.name)
}
var f1 = new Fn;
fi.getName  //rxy

如上代碼,在Fn.prototype.getName函數中,this指向的是f1對象。因此可以通過this.name獲取f1.name的值。
其實,不僅僅是構造函數的prototype,即便是在整個原型鏈中,this代表的也都是當前對象的值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.對象是什么 對象就是若干屬性的集合。 在JS中一切引用類型都是對象:數組是對象,函數是對象,對象還是對象。對象...
    liushaung閱讀 1,218評論 0 2
  • 原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-c...
    jaysoul閱讀 484評論 0 0
  • 1,javascript 基礎知識 Array對象 Array對象屬性 Arrray對象方法 Date對象 Dat...
    Yuann閱讀 945評論 0 1
  • 本文為深入理解javascript原型和閉包系列的摘要筆記 1.一切都是對象 對象:若干屬性的集合。 java或者...
    JYOKETSU3閱讀 303評論 0 0
  • 把瑩瑩送走,心里沒事了,把架子打開,干活 這么復雜,還真有點畏難情緒,又想等著人家幫忙,依賴心理太嚴重。還是自己動...
    不二努力閱讀 124評論 0 0