[day 02] new & factory: 如何建立一個新物件


※ 本文同步發表於隨性筆記

本系列文章討論JS 物件導向設計相關的特性。 不含CSS,不含HTML
建議先有些JS基礎再繼續閱讀。
你也可以看看從零開始遲來的Web開發筆記
雖然是「7天寫作松」挑戰,但同樣可以視為系列後續文章

No CSS! No HTML! No Browser!
Just need programming language


既然是要來物件導向,當然要先來學怎麼建立物件。本節帶你看看如何建立一個新的物件。

直接建立

obj0 = {
    name: "World",
    hello(){console.log(`Hello, ${this.name}`)},
}

obj0.hello();

工廠模式

可以直接建立物件,也可以透過工廠模式建立並初始化物件。

<!--more-->
function FactoryNew(name){
  /*
  var new_obj = {
    name: name,
    hello(){console.log(`Hello, ${this.name}`)},
  };
  */
  var new_obj = Object.create(obj0)
  new_obj.name = name
  return new_obj;
}

var obj1 = FactoryNew("Daniel");
obj1.hello() // => Hello, Daniel
obj0.hello() // => Hello, World

單純使用工廠模式太過簡單了,既然都叫原形繼承了,就用原形設計模式(prototye design pattern來做吧!

※ 上面兩種寫法在此作用等價

有些物件若以標準的方式建立實例,或者是設定至某個狀態需要複雜的運算及昂貴的資源,則您可以考慮直接以某個物件作為原型,在 需要個別該物件時,複製原型並傳回1

雖然上面使用了Object.create2來建立新物件,不過有些細節與原形設計模式還是不同,我以後會說到,這裡就姑且認為是複製物件內容到新物件裡去吧。

new Operator

在很早之前就有了new操作方法3。透過new,工廠模式可以更加簡單:

function FooConstructor(name){
  this.name = name;
  this.hello = function(){
     console.log(`Hello, ${this.name}`);
  }
}

var obj2 = new FooConstructor("Foo");
obj2.hello() // Hello, Foo

※ 注意!你當然可以是用Foo()的命名方式,這樣new Foo(name)會更明確好看,不過為了後續的內容,我加上了Contructor

new 發生了什麼事?

根據MDN是這麼寫的:

1. 創建一個空的簡單JavaScript對象(即{});
2. 鏈接該對象(即設置該對象的搆造函數)到另一個對象 ;
3. 將步驟1新創建的對象作為this的上下文 ;
4. 如果該函數沒有返回對象,則返回this。

看看第4點,是的我們剛剛沒有回傳值,當然你可以回傳個物件:

function Empty(name){
  this.name = name
  this.hello = function(){
     console.log(`Hello, ${this.name}`);
  }
  return {}; // 故意回傳一個空物件
}

empty_obj = new Empty("World")
// empty_obj.hello() // error! no hello() method
console.log(empty_obj) // => {}

來加點料

這小節稍微有點進階,看不懂可以先跳過。

var obj3 = {};
obj3.constructor = FooConstructor;
obj3.constructor("Kitty");
obj3.hello(); // => Hello, Kitty

var obj4 = {};
obj4.__proto__ = FooConstructor.prototype;
obj4.constructor("K-on"); // 完了,暴露宅屬性
obj4.hello(); // => Hello, K-on

Object.is(obj3.constructor, obj4.constructor);

我明天才會提到原形鏈相關的概念。這裡只要先知道obj3obj4FooConstructor都有關係就好。然後透過增加FooConstructor.prototype的新方法,可以使所有都有新的方法:

FooConstructor.prototype.bye = function(){console.log(`Bye Bye, ${this.name}`)};
obj2.bye(); // => Bye Bye, Foo
// obj3.bye(); // error! 沒繼承到`FooConstructor`,不受影響
obj4.bye(); // => Bye Bye, K-on

new.target

FooConstructor也是函式,可以直接呼叫,但可能有異想不到的結果:

var name; // just declare
console.log(name); // => underfined

FooConstructor("JavaScript");

// Oops! 怎麼被改到了??
console.log(name); // => JavaScript

欸欸!!為什麼name被改到了??

這是因為為特別確定this的話,會指向globalThis4物件。瀏覽器指window;在Node.js環境下是global

所以最好的方式是透過new.target5加以限制:

function FooConstructor(name){
  if (!new.target) throw "FooConstructor() must be called with new";
  this.name = name;
  this.hello = function(){
     console.log(`Hello, ${this.name}`);
  }
}

// FooConstructor("JavaScript"); // error! FooConstructor() must be called with new

[工商時間] 俄羅斯方塊 網頁遊戲

在ES6都還沒出來時,為了練習JS裡物件導向的概念,從頭不搞框架自幹了網頁遊戲--俄羅斯方塊。賞臉去玩一下吧!(儘管是陳舊的code,但應該還是有些值得看一下)

參考資料

#js #javascript #EMCAScript





寫了這麼久的JS,你還在物件之前的時代嗎?只有資料、函式可以用,破破的抽象化,不會難以維護?儘管JS起初並不以物件導向設計,但透過原形鏈設計,其仍然可以具有好維護的物件導向特色。本系列從最基礎的this,深入ES6之後的class。

留言討論