《JavaScript語言入門教程》記錄整理:面向對象

本系列基於阮一峰老師的《JavaScrip語言入門教程》或《JavaScript教程》記錄整理,教程採用知識共享 署名-相同方式共享 3.0協議。這幾乎是學習js最好的教程之一(去掉之一都不過分)

最好的教程而阮一峰老師又採用開源方式共享出來,之所以重新記錄一遍,一是強迫自己重新認真讀一遍學一遍;二是對其中知識點有個自己的記錄,加深自己的理解;三是感謝這麼好的教程,希望更多人閱讀了解

面向對象編程

實例對象與 new 命令

  1. 面向對象編程(Object Oriented ProgrammingOOP)將現實世界中的實物、邏輯操作及各種複雜關係抽象為一個個對象,每一個對象完成一定的功能,用來接受資訊、處理數據或執行操作、發布資訊等,通過繼承還能實現復用和功能擴展。比起由一系列函數或指令組成的傳統的過程式編程(procedural programming)更適合大型項目。

  2. 什麼是”對象”(object):(1)對象是單個實物的抽象。(2)對象是一個容器,封裝了屬性(property)和方法(method)。屬性是對象的狀態,方法是對象的行為(完成某種任務)。

  3. 生成對象時,通常需要一個模板,表示某一類實物的共同特徵,然後根據模板生成。在C++、java、c#等語言中都有類(class)的概念。”類”就是對象的模板,對象是”類”的實例(即類的一個具體對象)。JavaScript的對象體系基於構造函數(constructor)和原型鏈(prototype)構成。

  4. JavaScript 語言中構造函數(constructor)就是對象的模板,描述實例對象的基本結構。”構造函數”就是專門用來生成實例對象的函數。一個構造函數,可以生成多個實例對象,這些實例對象都有相同的結構。

  5. 構造函數和普通函數一樣,但是有自己的特徵和用法。

如下,Vehicle就是構造函數。通常構造函數名字第一個字母大寫(與普通函數作區分)。

var Vehicle = function () {
  this.price = 1000;
};

構造函數的特點

  • 函數體內部使用了this關鍵字,代表了所要生成的對象實例。
  • 生成對象的時候,必須使用new命令。
  1. new命令的作用是執行構造函數,返回一個實例對象。
var Vehicle = function () {
  this.price = 1000;
};

var v = new Vehicle();
v.price // 1000

如果忘記了new命令,就成了構造函數作為普通函數直接調用

為了保證構造函數必須使用new命令,解決辦法有兩種:

一、可以在構造函數內部使用嚴格模式。這樣不使用new命令直接調用就會報錯

var Vehicle = function () {
  'use strict';
  this.price = 1000;
};

var v = Vehicle();  // Uncaught TypeError: Cannot set property 'price' of undefined

嚴格模式中,函數內部的this不能指向全局對象,默認等於undefined,導致不加new調用會報錯

二、在構造函數內部判斷是否使用new命令,如果沒有,則根據參數返回一個實例對象。

function Vehicle(price) {
  if (!(this instanceof Vehicle)) {
    return new Vehicle(price);
  }

  this.price = price||1000;
};

var v1 = Vehicle();
var v2 = new Vehicle();
  1. 使用new命令時,後面的函數依次執行下面的步驟。
  • 創建一個空對象,作為將要返回的對象實例。
  • 將這個空對象的原型,指向構造函數的prototype屬性。
  • 將這個空對象賦值給函數內部的this關鍵字。
  • 開始執行構造函數內部的程式碼。

構造函數內部,this指的是一個新生成的空對象。構造函數的目的就是操作一個空對象(即this對象),將其”構造”為需要的樣子。

如果構造函數內部有return語句且return後面跟著一個對象,new命令會返回return語句指定的對象;否則,就會不管return語句,返回this對象。

var Vehicle = function () {
  this.price = 1000;
  return 1000;  // 忽略非對象的return語句
};

(new Vehicle()) === 1000

如果return返回的是其他對象而不是this,那麼new命令將會返回這個新對象

如果對普通函數(內部沒有this關鍵字的函數)使用new命令,則會返回一個空對象。

function getMessage() {
  return 'this is a message';
}

var msg = new getMessage();
msg // {}
typeof msg // "object"

new命令簡化的內部流程,可用下面的程式碼表示。

function _new(/* 構造函數 */ constructor, /* 構造函數參數 */ params) {
  // 將 arguments 對象轉為數組
  var args = [].slice.call(arguments);
  // 取出構造函數
  var constructor = args.shift();
  // 創建一個空對象,繼承構造函數的 prototype 屬性
  var context = Object.create(constructor.prototype);
  // 執行構造函數
  var result = constructor.apply(context, args);
  // 如果返回結果是對象,就直接返回,否則返回 context 對象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 實例
var actor = _new(Person, '張三', 28);
  1. 函數內部的new.target屬性。如果當前函數是new命令調用,new.target指向當前函數,否則為undefined
function f() {
  console.log(new.target === f);
}

f() // false
new f() // true

此屬性可判斷是否使用new命令調用了函數

function f() {
  if (!new.target) {
    throw new Error('請使用 new 命令調用!');
  }
  // ...
}

f() // Uncaught Error: 請使用 new 命令調用!
  1. Object.create() 創建實例對象

通常使用構造函數作為生成實例對象的模板。但是如果沒有構造函數只有對象時,可以使用Object.create()方法以一個對象作為模板,生成新的實例對象。

如下,對象person1person2的模板,後者繼承了前者的屬性和方法。

var person1 = {
  name: '張三',
  age: 38,
  greeting: function() {
    console.log('你好,我是' + this.name + '。');
  }
};

var person2 = Object.create(person1);
person2.name;        // "張三"
person2.name="李四"  // "李四"
person2.greeting()   // 你好,我是李四。

person1.greeting()  // 你好,我是張三。

this關鍵字

  1. this關鍵字總是返回一個對象,或指向一個對象。
  2. this就是屬性或方法”當前”所在的對象。也就是說,如果改變屬性或方法所在的對象,就可以改變this的指向

將對象的屬性賦給另一個對象,改變屬性所在對象,可以改變this的指向。

如下,通過改變函數f所在的對象,實現this的改變

function f() {
  return '姓名:'+ this.name;
}

var A = {
  name: '張三',
  describe: f
};

var B = {
  name: '李四',
  describe: f
};

f()          // "姓名:"
A.describe() // "姓名:張三"
B.describe() // "姓名:李四"

只要函數被賦給另一個變數,this的指向就會變。

  1. JavaScript中,一切皆對象。運行環境也是對象(頂層函數中,this指向window對象),函數都是在某個對象之中運行,this就是函數運行時所在的對象(環境)。同時this的指向是動態的

  2. this的本質或this的設計目的:

js的對象在記憶體的結構是這樣的,對象存在堆中,當把對象賦值給一個變數時,實際是將對象在堆中的記憶體地址賦值給變數。如下,將對象的地址(reference)賦值給變數obj

var obj = { foo:  5 };

讀取obj.foo的過程是,先從obj拿到記憶體地址,然後從該地址讀出原始的對象,返回它的foo屬性

原始的對象以字典結構保存,每一個屬性名都對應一個屬性描述對象。比如上面的屬性foo實際保存形式如下,foo屬性的值保存在屬性描述對象的value屬性裡面:

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

當屬性的值是函數時

var obj = { foo: function () {} };

js將函數單獨保存在記憶體中,將函數的地址賦值給foo屬性的value屬性。

{
  foo: {
    [[value]]: 函數的地址
    ...
  }
}

因為函數是單獨存在的值,所以可以在不同的環境(上下文)執行

JavaScript允許在函數體內部,引用當前環境的其他變數。

如下,函數體使用的變數x由運行環境提供。

var f = function () {
  console.log(x);
};

由於函數可以在不同的運行環境執行,所以需要一種機制,可以在函數體內部獲得當前的運行環境(context)。所以this就被用來設計為,在函數體內部,指代函數當前的運行環境

如下,函數體中this.x就指當前運行環境的x

var f = function () {
  console.log(this.x);
}
  1. this的使用場合
  • 全局環境使用this,指的是頂層對象window
  • 構造函數中的this,指的是實例對象。
  • 對象的方法裡面包含thisthis的指向就是方法運行時所在的對象。該方法賦值給另一個對象,會改變this的指向。

關於this的指向並不好把握,比如下面的例子

var obj ={
  foo: function () {
    console.log(this);
  }
};

obj.foo() // obj

如上,通過調用boj對象的foo方法,輸出this為當前的obj對象。但是,如果使用下面的形式,都會改變this的指向

// 情況一
(obj.foo = obj.foo)() // window
// 情況二
(false || obj.foo)() // window
// 情況三
(1, obj.foo)() // window

上面程式碼中,obj.foo是獲取出來之後再調用,相當於一個值,這個值在調用的時候,運行環境已經從obj變為了全局環境,this的指向變為了window

可以這樣理解,在js引擎內部,obj對象和obj.foo函數儲存在兩個記憶體地址,稱為地址一和地址二。obj.foo()調用時,是從地址一調用地址二,因此地址二的運行環境是地址一,this指向obj。上面三種情況,都是直接取出地址二進行調用(即取出函數調用),這樣的話,運行環境就是全局環境,this指向的是全局環境。上面三種情況等同於下面的程式碼:

// 情況一
(obj.foo = function () {
  console.log(this);
})()
// 等同於
(function () {
  console.log(this);
})()

// 情況二
(false || function () {
  console.log(this);
})()

// 情況三
(1, function () {
  console.log(this);
})()

this所在的方法不在對象的第一層時,這時this指向當前一層的對象(即當前所在的對象),而不會繼承更上面的層。

var a = {
  p: 'Hello',
  b: {
    m: function() {
      console.log(this.p);
    }
  }
};

a.b.m() // undefined
  1. this使用中注意點:
  • 避免多層this。用於this的指向可變,盡量不要在函數中包含多層this

通過添加指向this的變數,實現多層this的使用

var o = {
  f1: function() {
    console.log(this);
    var that = this;
    var f2 = function() {
      console.log(that);
    }();
  }
}

o.f1()
// Object
// Object

JavaScript嚴格模式下,如果函數內部的this指向頂層對象,就會報錯。

  • 避免使用數組處理方法(mapforeach方法中的參數函數)中的this

mapforeach方法的回調函數中的this指向window對象。解決辦法是使用一個中間變數固定this,或者使用this作為mapforeach方法的第二個參數

// 中間變數
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    var that = this;
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    });
  }
}

o.f()
// hello a1
// hello a2

// 第二個參數this
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}

o.f()
// hello a1
// hello a2
  • 回調函數中避免使用this(往往會改變指向)。
  1. this的動態切換,既體現了靈活,又使編程變得困難和模糊。js提供了callapplybind方法,來切換/固定this的指向。
  2. Function.prototype.call()函數實例call方法,可以指定函數內部this的指向(即函數執行時所在的作用域),然後在指定的作用域中調用該函數

如下,使用call改變作用域6

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true

call方法的第一個參數,應該是一個對象。如果參數為空、nullundefined,則this指向全局對象。

var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

call方法的第一個參數是一個原始值,則原始值會自動轉成對應的包裝對象,然後傳入call方法。

var f = function () {
  return this;
};

f.call(5)   // Number {[[PrimitiveValue]]: 5}

call方法除第一個參數表示調用函數的作用域,其他參數以列表的形式傳遞,表示函數執行時的參數

func.call(thisValue, arg1, arg2, ...)

call方法的一個應用是調用對象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false

// 覆蓋掉繼承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false
  1. Function.prototype.apply()apply方法的作用,也是改變this指向,然後再調用該函數。但是它接收的是一個數組作為函數執行時的參數,
func.apply(thisValue, [arg1, arg2, ...])

call一樣,第一個參數是this指向的對象。null或undefined表示全局對象。第二個參數是數組,表示傳入原函數的參數

apply數組,call列表

(1)找出數組最大元素

js默認沒有找出數組最大元素的函數,結合applyMath.max可實現返回數組的最大元素

var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15

(2)將數組的空元素變為undefined

結合applyArray構造函數將數組的空元素變成undefined

Array.apply(null, ['a', ,'b'])   // [ 'a', undefined, 'b' ]

forEach等循環方法會跳過空元素,但是不會跳過undefined

(3)轉換類似數組的對象

利用數組對象的slice方法,可以將一個類似數組的對象(如arguments對象)轉為真正的數組。

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, 空]
Array.prototype.slice.apply({length: 1}) // [空]

(4)綁定回調函數的對象

可以在事件方法等回調函數中,通過apply/call綁定方法調用的對象,修改this指向

var o = new Object();
o.f = function () {
  console.log(this === o);
}

var f = function (){
  o.f.apply(o);
  // 或者 o.f.call(o);
};

// jQuery 的寫法
$('#button').on('click', f);

因為apply()/call()方法在綁定函數執行時所在的對象時,還會立即執行函數,因此需要把綁定語句寫在一個函數體內。

  1. Function.prototype.bind()bind()方法將函數體內的this綁定到某個對象,然後返回一個新函數。

如下是一個通過賦值導致函數內部this指向改變的示例。

var d = new Date();
d.getTime() // 1596621203097

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

d.getTime賦值給變數print後,方法內部的this由原來指向Date對象實例改為了window對象,print()執行報錯。

使用bind()方法綁定函數執行的this指向,可以解決這個問題。

var print = d.getTime.bind(d);
undefined
print()   // 1596621203097

bind()可接受更多參數,將這些參數綁定原函數的參數。

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20

如上,bind()方法除了綁定this對象,還綁定add()函數的第一個參數x5,然後返回一個新函數newAdd(),這個函數只要再接受一個參數y就能運行了。

bind()第一個參數是nullundefined時,this綁定的是全局對象(瀏覽器環境為window)

  1. bind()方法特定:
  • 每一次返回一個新函數

這就導致,如果綁定事件時直接使用bind()會綁定為一個匿名函數,導致無法取消事件綁定

element.addEventListener('click', o.m.bind(o));
// 如下取消是無效的
element.removeEventListener('click', o.m.bind(o));

正確寫法:

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);
  • 結合回調函數使用。將包含this的方法直接當做回調函數,會導致函數執行時改變了this的指向,從而出錯。解決辦法是使用bind()方法綁定回調函數的this對象。當然,也可使用中間變數固定this

  • 結合call()方法使用。改寫一些JS原生方法的使用

如下數組的slice方法

[1, 2, 3].slice(0, 1) // [1]
// 等同於
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

call()方法實質上是調用Function.prototype.call()方法。

// 上面等同於
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

相當於在Array.prototype.slice調用Function.prototype.call,參數為(對象,slice的參數)

類似的寫法:

var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);

var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]

pop(a)
a // [1, 2, 3]

更進一步bind的調用也可以改寫:在Function.prototype.bind上調用call方法(返回的是一個新方法),方法參數是(this對象,bind方法參數)。即最終結果是在this對象上執行bind方法並傳遞參數。(有些繞)

function f() {
  console.log(this.v);
}

var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123

對象的繼承

  1. 對象的繼承可以實現程式碼的復用
  2. 傳統JavaScript的繼承是通過”原型對象”(prototype)實現的。即js的原型鏈繼承。ES6引入了class語法,實現基於class的繼承
  3. 構造函數的缺點:構造函數中通過給this對象的屬性賦值,可以很方便地定義實例對象屬性。但是這種方式,同一個構造函數的多個實例之間無法共享屬性。
function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.features = {
    species:'貓',
    habits:'肉食夜行動物'
  };
  this.meow = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow   // false
cat1.features === cat2.features   // false

cat1cat2是同一個構造函數的兩個實例,因為所有meow方法和features對所有實例具有同樣的行為和屬性,應該共享而不是每個實例都創建新的方法和屬性,沒必要又浪費系統資源。

原型對象(prototype)用來在實例間共享屬性。

  1. JavaScript繼承機制的設計思想:原型對象的所有屬性和方法,都能被實例對象共享
  2. JavaScript規定,每個函數都有一個prototype屬性,指向一個對象
function f() {}
typeof f.prototype // "object"

普通函數基本不會用prototype屬性

構造函數生成實例的時候,構造函數的prototype屬性會自動成為實例對象的原型。

function Cat(name, color) {
  this.name = name;
}
Cat.prototype.color = 'white';
Cat.prototype.features = {
    species:'貓',
    habits:'肉食夜行動物'
  };
Cat.prototype.meow = function () {
    console.log('喵喵');
  };
var cat1 = new Cat('大毛');
var cat2 = new Cat('二毛');

原型對象的屬性不是實例對象自身的屬性。其變動體現在所有實例對象上。

當實例對象本身沒有某個屬性或方法的時候,它會到原型對象去尋找該屬性或方法。如果實例對象自身就有某個屬性或方法,則不會再去原型對象尋找這個屬性或方法。

原型對象的作用,是定義所有實例對象共享的屬性和方法。這也是被稱為原型對象的原因。實例對象可以視作從原型對象衍生出來的子對象。

  1. JavaScript規定,所有對象都有自己的原型對象(prototype)。任何一個對象,都可以充當其他對象的原型;而由於原型對象也是對象,所以它也有自己的原型。這就形成一個”原型鏈”(prototype chain):對象到原型,再到原型的原型…

  2. 所有對象的原型最終都可以上溯到Object.prototype,即Object構造函數的prototype屬性。所有對象都繼承了Object.prototype的屬性。

比如所有對象都有valueOftoString方法,就是從Object.prototype繼承的

Object.prototype對象的原型是null。原型鏈的盡頭是null

null沒有任何屬性和方法,也沒有自己的原型

Object.getPrototypeOf(Object.prototype)  // null
  1. 如果對象自身和它的原型,都定義了一個同名屬性,則優先讀取對象自身的屬性,這叫做”覆蓋”(overriding)。

  2. prototype對象有一個constructor屬性,默認指向prototype對象所在的構造函數。

function P() {}
P.prototype.constructor === P // true

constructor屬性的作用是,可以得知某個實例對象由哪一個構造函數產生。另外,有了constructor屬性就可以從一個實例對象新建另一個實例。

function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true

藉助constructor可以在實例方法中調用自身的構造函數

Constr.prototype.createCopy = function () {
  return new this.constructor();
};
  1. constructor屬性表明了原型對象與構造函數之間的關聯關係。因此如果修改原型對象,一般需要同時修改constructor屬性
function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person // true

Person.prototype = {
  method: function () {}
};

Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true

修改原型對象時,一般要同時修改constructor屬性的指向

// 壞的寫法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

// 好的寫法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
};

// 更好的寫法
C.prototype.method1 = function (...) { ... };
  1. constructor屬性的name屬性返回構造函數的名稱。

  2. instanceof表示對象是否為某個構造函數的實例。instanceof做判斷時會檢查右邊構造函數的原型對象(prototype)是否在左邊實例對象的原型鏈上。

v instanceof Vehicle
// 等同於
Vehicle.prototype.isPrototypeOf(v)

instanceof會檢查整個原型鏈,因此使用instanceof判斷時,實例對象的原型鏈上可能返回多個構造函數的原型對象

var d = new Date();
d instanceof Date // true
d instanceof Object // true

任意對象(除了null)都是Object的實例。

var nullObj=null;
typeof nullObj === 'object' && !(nullObj instanceof Object);  // true

如果一個對象的原型是nullinstanceof的判斷就會失真。

利用instanceof可以解決調用構造函數時忘了加new的問題

  1. 構造函數的繼承

子類整體繼承父類

一、在子類的構造函數中調用父類的構造函數

function Sub(value) {
  Super.call(this); // 繼承父類實例的屬性
  this.prop = value;
}

// 或者使用另一種寫法
function Sub() {
  this.base = Super;
  this.base();
}

二、讓子類的原型指向父類的原型,繼承父類原型

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';

使用Object.create(Super.prototype)賦值給子類的原型,防止引用賦值,後面的修改影響父類的原型。

上面是比較正確或嚴謹的寫法。比較粗略的寫法是直接將一個父類實例賦值給子類的原型

Sub.prototype = new Super();

這種方式在子類中會繼承父類實例的方法(通常可能不需要具有父類的實例方法),不推薦

子類中繼承父類的單個方法

ClassB.prototype.print = function() {
  ClassA.prototype.print.call(this);
  // self code
}
  1. 多重繼承:JavaScript不提供多重繼承功能,即不允許一個對象同時繼承多個對象。

但是可以通過合併兩個父類的原型的形式,間接變通的實現多重繼承

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  M1.call(this);
  M2.call(this);
}

// 繼承 M1
S.prototype = Object.create(M1.prototype);
// 繼承鏈上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定構造函數
S.prototype.constructor = S;

var s = new S();
s.hello // 'hello'
s.world // 'world'

這種子類S同時繼承了父類M1M2的模式又稱為 Mixin(混入)

  1. JavaScript不是一種模組化程式語言,ES6才開始支援”類”和”模組”。但是可以利用對象實現模組的效果
  2. 模組是實現特定功能的一組屬性和方法的封裝。所以模組的實現最簡單的方式就是把模組寫成一個對象,所有模組成員都位於對象裡面
  • 把模組寫成一個對象
var module1 = new Object({
 _count : 0,
 m1 : function (){
  //...
 },
 m2 : function (){
   //...
 }
});

函數m1m2和屬性_count都封裝在module1對象中。使用中直接調用這個對象的屬性即可。

但是,這種寫法暴露了所有的模組成員,內部狀態可以被外部改寫。比如,在外部直接改寫內部_count的值:module1._count = 5;

  • 使用構造函數封裝私有變數

如下,通過構造函數封裝實例的私有變數

function StringBuilder() {
  var buffer = [];

  this.add = function (str) {
     buffer.push(str);
  };

  this.toString = function () {
    return buffer.join('');
  };
}

如下,私有變數buffer在實例對象中,外部是無法直接訪問的。

但是,這種方法將私有變數封裝在構造函數中,構造函數會和實例對象一直存在於記憶體中,無法在使用完成後清除。即構造函數的作用既用來生成實例對象,又用來保存實例對象的數據,違背了構造函數與實例對象在數據上相分離的原則(即實例對象的數據,不應該保存在實例對象以外)。同時佔用記憶體。

  • 構造函數中將私有變數設置為實例屬性
function StringBuilder() {
  this._buffer = [];
}

StringBuilder.prototype = {
  constructor: StringBuilder,
  add: function (str) {
    this._buffer.push(str);
  },
  toString: function () {
    return this._buffer.join('');
  }
};

這樣私有變數就放在了實例對象中。但是私有變數仍然可以從外部讀寫

  • 通過立即執行函數封裝私有變數

通過”立即執行函數”(Immediately-Invoked Function ExpressionIIFE),通過返回”閉包”的方法和屬性,實現將屬性和方法封裝在一個函數作用域裡面,函數內的屬性作為私有成員不被暴露。

這就是js模組的基本寫法:

var module1 = (function () {
 var _count = 0;
 var m1 = function () {
   //...
 };
 var m2 = function () {
  //...
 };
 return {
  m1 : m1,
  m2 : m2
 };
})();
  • 模組的放大模式
    如果一個模組很大,必須分成幾個部分,或者一個模組需要繼承另一個模組,這時可以採用”放大模式”(augmentation)。

如下,為模組module1添加新方法,並返回新的module1模組

var module1 = (function (mod){
 mod.m3 = function () {
  //...
 };
 return mod;
})(module1);
  • “寬放大模式”(Loose augmentation)

在立即執行函數的參數中添加空對象,防止載入一個不存在的對象,從而報錯或出意外

var module1 = (function (mod) {
 //...
 return mod;
})(window.module1 || {});
  • 全局變數的輸入

模組最重要的是”獨立性”。因此為了在模組內部調用(使用)全局變數,必須顯式地將其他變數輸入模組內。

比如,下面module1用到了jQuery庫(模組),則可以將其作為參數輸入module1。保證模組的獨立性,並且表明模組之間的依賴關係

var module1 = (function ($) {
 //...
})(jQuery);

立即執行函數還可以起到類似命名空間的作用

Object對象的方法

  1. Object.getPrototypeOf方法返回參數對象的原型。這是獲取原型對象的標準方法。

幾種特殊的原型:

// 空對象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true

// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true

// 函數的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
  1. Object.setPrototypeOf方法為參數對象設置原型,返回該參數對象。Object.setPrototypeOf(obj,prototypeObj)

new命令可以使用Object.setPrototypeOf方法模擬。

var F = function () {
  this.foo = 'bar';
};
var f = new F();

// 等同於
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
  1. Object.create方法以一個對象為原型,返回一個實例對象。該實例完全繼承原型對象的屬性。
// 原型對象
var A = {
  print: function () {
    console.log('hello');
  }
};

// 實例對象
var B = Object.create(A);

Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true

Object.create方法的實現可以用下面的程式碼代替

if (typeof Object.create !== 'function') {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

生成新的空對象,如下四種是等價的

var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
var obj4 = {};

Object.create的參數為null可以生成一個不繼承任何屬性(沒有toStringvalueOf方法)的對象

var obj = Object.create(null);

Object.create方法必須指定參數且為對象,否則報錯。Object.create創建的對象的原型是引用賦值,即動態繼承原型。

Object.create方法還可以接受的第二個參數是屬性描述對象,描述的對象屬性會添加到實例對象的自身屬性上。

var obj = Object.create({}, {
  p1: {
    value: 123,
    enumerable: true,
    configurable: true,
    writable: true,
  },
  p2: {
    value: 'abc',
    enumerable: true,
    configurable: true,
    writable: true,
  }
});

// 等同於
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';

Object.create方法生成的對象會繼承它的原型對象的構造函數。

  1. Object.prototype.isPrototypeOf():實例對象的isPrototypeOf方法判斷該對象是否為參數對象原型鏈上的原型。

Object.prototype位於除了直接繼承自null的對象之外的所有對象的原型鏈上。

Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
  1. 關於__proto__屬性。__proto__屬性是實例對象的屬性,表示實例對象的原型(可讀寫)。實例對象(或非函數對象)無法通過prototype屬性獲取原型(只有參數才有prototype屬性),而__proto__屬性默認應該是私有屬性,不應該被讀寫,並且__proto__屬性只有瀏覽器才需要部署。因此,對原型的讀寫操作正確做法是使用Object.getPrototypeOf()Object.setPrototypeOf()

Obj可以用__proto__直接設置原型

  1. 關於__proto__prototype屬性

如下,為構造函數、實例對象、普通對象中__proto__和prototype的對比

/** 構造函數的__proto__和prototype **/
var P=function(){}

P.prototype
// {constructor: ƒ}

P.__proto__
// ƒ () { [native code] }

P.__proto__===P.prototype
// false

P.__proto__===P.constructor.prototype
// true

P.__proto__===Object.getPrototypeOf(P)
// true

P.__proto__===Function.prototype
// true

P.constructor===Function
// true

/** 實例對象的__proto__和prototype  **/ 
var p=new P()

p.prototype
// undefined
p.__proto__
// {constructor: ƒ}
p.__proto__===Object.getPrototypeOf(p)
// true

p.__proto__===P
// false
p.__proto__===P.prototype
// true

p.constructor===P
// true

/** 實例對象的__proto__和prototype **/
var obj={}

obj.prototype
// undefined

obj.__proto__===Object.getPrototypeOf(obj)
// true

obj.__proto__===Object.prototype
// true

obj.constructor===Object
// true

var nullObj=Object.create(null)

nullObj.__proto__
// undefined
nullObj
// {}無屬性

幾點總結:

  • js中,對象的原型通過__proto__屬性獲取,由此組成原型鏈及原型鏈的繼承。

  • __proto__是對象自帶的屬性,除了null和原型對象為null的對象之外,所有的對象都有__proto__屬性。函數是對象,因此函數也有__proto__屬性

  • prototype屬性是函數獨有的屬性,每個函數都有一個prototype屬性對象,作用是在實例對象間共享屬性和方法。因此prototype只會在構造函數中使用,表示實例對象的原型對象。面向對象中的繼承由此實現。

  • __proto__屬性指向當前對象的原型對象,即構造函數的prototype屬性。

  • constructor屬性表示當前對象的構造函數

  • 函數也是對象,因此也擁有__proto__屬性,指向當前函數的構造函數的prototype屬性。一個函數的constructorFunction__proto__Function.prototype

  1. __proto__屬性指向當前對象的原型對象,即構造函數的prototype屬性。
var obj = new Object();

obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
  1. 獲取一個對象obj的原型對象,有三種辦法:
  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

但是 __proto__屬性只有瀏覽器環境才需要部署。obj.constructor.prototype在手動改變原型對象時,可能會失效

如下,將構造函數C的原型對象改為p後。實例對象c.constructor.prototype卻沒有指向pObject.getPrototypeOf(obj)正確獲取原型對象,是獲取原型對象推薦使用的方法

var P = function () {};
var p = new P();

var C = function () {};
C.prototype = p;
var c = new C();

c.constructor.prototype === p // false

c.constructor.prototype === P.prototype   // true

Object.getPrototypeOf(c) === p  // true

上面變更原型對象的方法是不正確的。通常修改prototype時,要同時設置constructor屬性。

C.prototype = p;
C.prototype.constructor = C;

var c = new C();
c.constructor.prototype === p // true
  1. Object.getOwnPropertyNames()返回對象自身所有屬性的鍵名組成的數組(包括可遍歷和不可遍歷的所有屬性)。

  2. Object.keys返回對象自身所有可遍歷的屬性名組成的數組

  3. Object.prototype.hasOwnProperty()返回一個屬性是否為對象自身的屬性

hasOwnProperty方法是 JavaScript 之中唯一一個處理對象屬性時,不會遍歷原型鏈的方法

  1. in運算符表示一個對象是否具有某個屬性。即檢查一個屬性是否存在。
'length' in Date // true
'toString' in Date // true

for...in循環可以獲取一個對象所有可遍歷的屬性(自身和繼承的屬性)

通常使用如下方式,遍歷對象自身的屬性

for ( var name in object ) {
  if ( object.hasOwnProperty(name) ) {
    /* loop code */
  }
}
  1. 獲取一個對象的所有屬性(包含自身的和繼承的,以及可枚舉和不可枚舉的所有屬性)
function inheritedPropertyNames(obj) {
  var props = {};
  while(obj) {
    Object.getOwnPropertyNames(obj).forEach(function(p) {
      props[p] = true;
    });
    obj = Object.getPrototypeOf(obj);
  }
  return Object.getOwnPropertyNames(props);
}
  1. 對象的拷貝

要拷貝一個對象,需要做到下面兩點:

  • 確保拷貝後的對象,與原對象具有同樣的原型。
  • 確保拷貝後的對象,與原對象具有同樣的實例屬性。

如下,為對象拷貝的實現:

function copyObject(orig) {
  var copy = Object.create(Object.getPrototypeOf(orig));
  copyOwnPropertiesFrom(copy, orig);
  return copy;
}

function copyOwnPropertiesFrom(target, source) {
  Object
    .getOwnPropertyNames(source)
    .forEach(function (propKey) {
      var desc = Object.getOwnPropertyDescriptor(source, propKey);
      Object.defineProperty(target, propKey, desc);
    });
  return target;
}

利用ES2017引入的Object.getOwnPropertyDescriptors可以更簡便的實現

function copyObject(orig) {
  return Object.create(
    Object.getPrototypeOf(orig),
    Object.getOwnPropertyDescriptors(orig)
  );
}

嚴格模式(strict mode)

  1. JavaScript提供程式碼執行的第二種模式:嚴格模式。嚴格模式從ES5引入,主要目的為:
  • 明確禁止一些不合理、不嚴謹的語法,減少 JavaScript 語言的一些怪異行為。
  • 增加更多報錯的場合,消除程式碼運行的一些不安全之處,保證程式碼運行的安全。
  • 提高編譯器效率,增加運行速度。
  • 為未來新版本的 JavaScript 語法做好鋪墊。
  1. 嚴格模式的啟用:在程式碼頭部添加一行'use strict';即可。老版本的引擎會把它當作一行普通字元串,加以忽略。新版本的引擎就會進入嚴格模式。
  2. use strict放在腳本文件的第一行,整個腳本都將以嚴格模式運行。不在第一行則無效。
  3. use strict放在函數體的第一行,則整個函數以嚴格模式運行。
  4. 有時需要把不同腳本文件合併到一個文件。這時,如果一個是嚴格模式另一個不是,則合併後結果將會是不正確的。解決辦法是可以把整個腳本文件放在一個立即執行的匿名函數中:
(function () {
  'use strict';
  // some code here
})();
  1. 嚴格模式下的顯式報錯

嚴格模式下js的語法更加嚴格,許多在正常模式下不會報錯的錯誤程式碼都會顯式的報錯

如下幾項操作嚴格模式下都會報錯:

  • 只讀屬性不可寫;比如字元串的length屬性

  • 不可配置屬性無法刪除(non-configurable)

  • 只設置了取值器的屬性不可寫

  • 禁止擴展的對象不可擴展

  • evalarguments 不可用作標識名

正常模式下,如果函數有多個重名的參數,可以用arguments[i]讀取。嚴格模式下屬於語法錯誤。

  • 函數不能有重名的參數

  • 禁止八進位的前綴0表示。八進位使用數字0和字母O表示

  1. 嚴格模式下的安全限制
  • 全局變數顯式聲明
  • 禁止this關鍵字指向全局對象。避免無意中創造全局變數
// 正常模式
function f() {
  console.log(this === window);
}
f() // true

// 嚴格模式
function f() {
  'use strict';
  console.log(this === undefined);
}
f() // true

嚴格模式下,函數直接調用時,內部的this表示undefined(未定義),因此可以用callapplybind方法,將任意值綁定在this上面。正常模式下,this指向全局對象,如果綁定的值是非對象,將被自動轉為對象再綁定上去,而nullundefined這兩個無法轉成對象的值,將被忽略。

  • 函數內部禁止使用 fn.calleefn.caller

  • 禁止使用arguments.calleearguments.caller

arguments.calleearguments.caller是兩個歷史遺留的變數,從來沒有標準化過,現在已經取消

  • 禁止刪除變數。嚴格模式下使用delete命令刪除一個變數,會報錯。只有對象的屬性,且屬性的描述對象的configurable屬性設置為true,才能被delete命令刪除。
  1. 靜態綁定
  • 禁止使用with語句

  • 創設eval作用域

正常模式下,JavaScript語言有兩種變數作用域(scope):全局作用域和函數作用域。嚴格模式創設了第三種作用域:eval作用域。

eval所生成的變數只能用於eval內部。

(function () {
  'use strict';
  var x = 2;
  console.log(eval('var x = 5; x')) // 5
  console.log(x) // 2
})()

eval語句使用嚴格模式:

// 方式一
function f1(str){
  'use strict';
  return eval(str);
}
f1('undeclared_variable = 1'); // 報錯

// 方式二
function f2(str){
  return eval(str);
}
f2('"use strict";undeclared_variable = 1')  // 報錯
  • arguments不再追蹤參數的變化。嚴格模式下參數修改,arguments不再聯動跟著改變
  1. 面向ECMAScript 6
  • ES5的嚴格模式只允許在全局作用域或函數作用域聲明函數。
  • 保留字。嚴格模式新增了一些保留字:implementsinterfaceletpackageprivateprotectedpublicstaticyield