JavaScript基礎鞏固系列——面向對象編程(構造函數、new、對象繼承、對象拷貝、嚴格模式)

全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/13745195.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)

重新學習JavaScript是因爲當年轉前端有點兒趕鴨子上架的意味,我一直在反思我的知識點總是很零散,不能在腦海中形成一個完整的體系,所以這次想通過再次學習將知識點都串聯起來,結合日常開發的項目,達到溫故而知新的效果。與此同時,總結一下我認爲很重要但又被我遺漏的知識點~

構造函數

  • 不使用new命令調用構造函數會導致函數內部原本指向實例對象的this指向全局對象,致使構造函數中的變量、方法成爲全局變量、全局方法。
// 方案一:在構造函數內部使用嚴格模式(嚴格模式中,函數內部的this不能指向全局對象)
function Fubar(foo, bar){
  'use strict';
  this._foo = foo;
  this._bar = bar;
}

Fubar()
// TypeError: Cannot set property '_foo' of undefined

// 方案二:構造函數內部判斷是否使用new命令,如果發現沒有使用,則直接返回一個實例對象
function Fubar(foo, bar) {
  if (!(this instanceof Fubar)) {
    return new Fubar(foo, bar);
  }
  
  // 還可以通過new.target判斷,如果當前函數是new命令調用,new.target指向當前函數,否則爲undefined。
  // if(!new.target) {
  //   return new Fubar(foo, bar);
  // }

  this._foo = foo;
  this._bar = bar;
}

Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1
  • 如果構造函數內部有return語句,且return後面跟着一個對象,new命令會返回return語句指定的對象;否則,就會不管return語句,返回this對象。
var Vehicle = function () {
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000  // false

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

(new Vehicle()).price
// 2000

new命令原理

使用new命令時,它後面的函數依次執行下面的步驟:

  • 創建一個空對象,作爲將要返回的對象實例。
  • 將這個空對象的原型,指向構造函數的prototype屬性。
  • 將這個空對象賦值給函數內部的this關鍵字。
  • 開始執行構造函數內部的代碼。
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);

可以使用Object.setPrototypeOf()方法模擬:

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

var f = new F();
// 等同於
// 將一個空對象的原型設爲構造函數的prototype屬性(F.prototype)
var f = Object.setPrototypeOf({}, F.prototype);
// 將構造函數內部的this綁定這個空對象,然後執行構造函數,使得定義在this上面的方法和屬性(this.foo),都轉移到這個空對象上。
F.call(f);

對象的繼承

原型對象

  • 原型對象可以解決構造函數在實現代碼複用時多個實例之間無法共享屬性從而導致系統資源浪費的問題,原型對象的所有屬性和方法都能被實例對象共享。
function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"
  • 對於構造函數來說,生成實例的時候prototype屬性會自動成爲實例對象的原型,對象到原型,再到原型的原型...形成原型鏈,原型鏈的盡頭是null
Object.getPrototypeOf(Object.prototype)
// null
  • prototype對象有一個constructor屬性,默認指向prototype對象所在的構造函數,可以被所有實例對象繼承。
function P() {}
var p = new P();

p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false
  • 可以通過constructor屬性從一個實例對象新建另一個實例。
function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true
  • 如果不能確定constructor屬性是什麼函數,可以通過name屬性從實例得到構造函數的名稱。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
  • 修改原型對象時需要同時修改constructer屬性的指向。
// 壞的寫法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

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

// 更好的寫法
C.prototype.method1 = function (...) { ... };
  • Object.prototype.__proto__可以獲取實例對象的原型,只有瀏覽器需要部署,其他環境可以沒有這個屬性,所以儘量不用,應使用Object.PrototypeOf()Object.setPrototypeOf()進行讀寫操作。
var obj = {};
var p = {};

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

instanceof運算符

  • instanceof返回一個布爾值,表示對象是否爲某個構造函數的實例。
var d = new Date();
d instanceof Date // true
d instanceof Object // true

// 等同於
Date.prototype.isPrototypeOf(d)  // true
  • 由於任意對象(除了null)都是Object的實例,所以instanceof運算符可以判斷一個值是否爲非null的對象。
var obj = { foo: 123 };
obj instanceof Object // true

null instanceof Object // false
undefined instanceof Object // false
  • instanceof運算符只能用於對象,不適用原始類型的值。
var s = 'hello';
s instanceof String // false

構造函數的繼承

  • 讓一個構造函數繼承另一個構造函數分兩步:
// 第一步:在子類的構造函數中,調用父類的構造函數(在實例上調用父類的構造函數Super,就會讓子類實例具有父類實例的屬性)
function Sub(value) {
  Super.call(this);
  this.prop = value;
}
// 第二步:讓子類的原型指向父類的原型(這樣子類就可以繼承父類原型)
// 不是直接等於Super.prototype,否則後面對Sub.prototype的操作,會連父類的原型Super.prototype一起修改掉
Sub.prototype = Object.create(Super.prototype);  
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
  • 子類繼承父類的單個方法
ClassB.prototype.print = function() {
  ClassA.prototype.print.call(this);
  // some code
}

多重繼承

JavaScript 不提供多重繼承功能,即不允許一個對象同時繼承多個對象。但是,可以通過變通方法,實現這個功能。

// 子類S同時繼承了父類M1和M2,這種模式又稱爲 Mixin(混入)。
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'

對象的拷貝

如果要拷貝一個對象,需要做到下面兩件事情。

  • 確保拷貝後的對象,與原對象具有同樣的原型。
  • 確保拷貝後的對象,與原對象具有同樣的實例屬性。
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)
  );
}

嚴格模式

設計背景

早期的 JavaScript 語言有很多設計不合理的地方,但是爲了兼容以前的代碼,又不能改變老的語法,只能不斷添加新的語法,引導程序員使用新語法。
嚴格模式是從 ES5 進入標準的,主要目的有以下幾個:

  • 明確禁止一些不合理、不嚴謹的語法,減少 JavaScript 語言的一些怪異行爲。
    • 全局變量顯式聲明。
    • 禁止this關鍵字指向全局對象。
    • 禁止使用fn.calleefn.caller
    • 禁止使用arguments.calleearguments.caller
    • 禁止刪除變量。
  • 增加更多報錯的場合,消除代碼運行的一些不安全之處,保證代碼運行的安全。
    • 只讀屬性不可寫,例如對字符串的length屬性賦值:'abc'.length = 5
    • 刪除不可配置屬性(configurable: false)。
    • 只設置了取值器的屬性不可寫。
    • 禁止擴展的對象不可擴展:Object.preventExtensions(obj);obj.v = 1;
    • evalarguments不可用作標識名。
    • 函數不能有重名的參數:function f(a, a, b) {}
    • 禁止八進制的前綴0表示法:var n = 0100;
  • 提高編譯器效率,增加運行速度。
    • 禁止使用with語句。
    • 創設eval作用域。
    • arguments不再追蹤參數的變化。
    function f(a) {
      a = 2;
      return [a, arguments[0]];
    }
    f(1); // 正常模式爲[2, 2]
    
    function f(a) {
      'use strict';
      a = 2;
      return [a, arguments[0]];
    }
    f(1); // 嚴格模式爲[2, 1]
    
  • 爲未來新版本的 JavaScript 語法做好鋪墊。
    • 非函數代碼塊不得聲明函數,例如在條件代碼塊、循環代碼塊中。
    • 新增保留字implementsinterfaceletpackageprivateprotectedpublicstaticyield

啓用方法

在單個腳本的開頭或者某個函數的開頭使用字符串'use strict'

<script>
  'use strict';
  console.log('這是嚴格模式');
</script>
function strict() {
  'use strict';
  return '這是嚴格模式';
}

// 不同模式的腳本合併時應把整個腳本文件放在一個立即執行的匿名函數之中,防止出錯。
(function () {
  'use strict';
  // some code here
})();

參考資料

JavaScript 語言入門教程 :https://wangdoc.com/javascript/index.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章