JS 的 new 是個啥?
本文寫于 2019 年 11 月 25 日
new關鍵字在很多語言里面,總是用于把類實體化,可是 JS 之前就沒有“類”這個概念呀,
那 JS 的new有啥用?
這就要從 JS 的面向物件開始談起了,
我學習 JS 基本上是從 ES6 開始的,所以一直沒太管之前的面向物件,
最近在看 2012 年出版的紅寶書用來復習的時候,突然發現原來的面向物件,其實還是蠻惡心的,
并且即使是 ES6 的面向物件,他也是基于原型鏈的,這個包裝嚴格來說,只是語法糖而已,
OOP 是啥
首先簡單介紹一下 OOP,面向物件,
面向物件編程(Object Oriented Programming,縮寫為 OOP)是目前主流的編程范式,它將真實世界各種復雜的關系,抽象為一個個物件,然后由物件之間的分工與合作,完成對真實世界的模擬,
這個學過 Java 或者 C#之類的同學應該都懂,
首先有兩點:
-
物件是單個實物的抽象
一本書、一輛汽車、一個人都可以是物件,當實物被抽象成物件,實物之間的關系就變成了物件之間的關系,從而就可以模擬現實情況,針對物件進行編程, -
物件是一個容器,封裝了屬性(property)和方法(method)
屬性是物件的狀態,方法是物件的行為(完成某種任務),比如,我們可以把動物抽象為 animal 物件,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等),
可以說,OOP 是目前為止,最符合人類思維方式的編程了,
沒有 class 的 JS
曾經的 JS 根本沒有 class 這個說法,那 JS 是怎么批量創建物件的嗎?
let a = {
name: 'a',
age: 18
}
let b = {
name: 'b',
age: 14
}
let c = {
name: 'c',
age: 20
}
難道會像這樣一個一個的創建嗎?
那也太愚蠢了,并且一旦需要修改,就要修改每一項!
那怎么改呢?
可以利用原型——公共的一塊記憶體,
function createPerson(name) {
const obj = Object.create(createPerson.personPrototype)
obj.name = name
return obj
}
createPerson.personPrototype = {
sayName() {
return `你好,我叫${this.name},`
},
constructor: createPerson
}
這套操作就非常神奇了!
一旦我們有一個陣列:['Jack', '二狗', 'Rose', '李鐵蛋'],
該陣列需要被創建成 Person 物件,就可以這么操作了:
const arr = ['Jack', '二狗', 'Rose', '李鐵蛋']
const person = []
for (let name of arr) {
const p = createPerson(name)
console.log(`我的創造者是:${p.constructor}`)
person.push(p)
}
我們在 personPrototype 中,記錄了建構式;在建構式中,利用 personPrototype 構造了新的物件,
所以我們可以隨時通過建構式構造新物件;也可以隨時查看物件的建構式!
非常 amazing 呀!
既然這個方法這么好,所以,JS 的prototype就是如此運作的!
接下來看一下,具體的細節,
建構式
面向物件編程的第一步,就是要生成物件,(要是還找不到物件,就 new 一個好了,)
前面說過,物件是單個實物的抽象,通常需要一個模板,表示某一類實物的共同特征,然后物件根據這個模板生成,
典型的面向物件編程語言(比如 C++ 和 Java),都有“類”(class)這個概念,
所謂“類”就是物件的模板,物件就是“類”的實體,
但是,JavaScript 語言的物件體系,不是基于“類”的,而是基于建構式(constructor)和原型鏈(prototype),
JavaScript 語言使用建構式(constructor)作為物件的模板,
所謂”建構式”,就是專門用來生成實體物件的函式,
它就是物件的模板,描述實體物件的基本結構,一個建構式,可以生成多個實體物件,這些實體物件都有相同的結構,
建構式就是一個普通的函式,但是有自己的特征和用法:
let Vehicle = function () {
this.price = 1000
}
上面代碼中,Vehicle 就是建構式,為了與普通函式區別,建構式名字的第一個字母通常大寫,
建構式的特點有兩個,
- 函式體內部使用了 this 關鍵字,代表了所要生成的物件實體,
- 生成物件的時候,必須使用 new 命令,
new 一個物件
new 命令的作用,就是執行建構式,回傳一個實體物件,
let Vehicle = function () {
this.price = 1000
}
let v = new Vehicle()
v.price // 1000
上面代碼通過 new 命令,讓建構式 Vehicle 生成一個實體物件,保存在變數 v 中,
這個新生成的實體物件,從建構式 Vehicle 得到了 price 屬性,
new 命令執行時,建構式內部的 this,就代表了新生成的實體物件,this.price 表示實體物件有一個 price 屬性,值是 1000,
使用 new 命令時,根據需要,建構式也可以接受引數:
let Vehicle = function (p) {
this.price = p
}
let v = new Vehicle(500)
new 命令本身就可以執行建構式,所以后面的建構式可以帶括號,也可以不帶括號,
(這里我的編輯器會強行給我補上括號,如果發布的文章,倆都有括號,代表我不小心格式化了這篇文章)
下面兩行代碼是等價的,但是為了表示這里是函式呼叫,推薦使用括號,
// 推薦的寫法
let v = new Vehicle()
// 不推薦的寫法
let v = new Vehicle
那如果忘了使用 new 命令,直接呼叫建構式會發生什么事?
這種情況下,建構式就變成了普通函式,并不會生成實體物件,而且由于后面會說到的原因,this 這時代表全域物件,將造成一些意想不到的結果,
let Vehicle = function () {
this.price = 1000
}
let v = Vehicle()
v // undefined
price // 1000
上面代碼中,呼叫 Vehicle 建構式時,忘了加上 new 命令,結果,變數 v 變成了 undefined,而 price 屬性變成了全域變數,
因此,應該非常小心,避免不使用 new 命令、直接呼叫建構式,
為了保證建構式必須與 new 命令一起使用,一個解決辦法是,建構式內部使用嚴格模式,即第一行加上 use strict,
這樣的話,一旦忘了使用 new 命令,直接呼叫建構式就會報錯,
(其實有了新的 ES6 語法,這就沒有必要了)
function Fubar(foo, bar) {
'use strict'
this._foo = foo // 這種寫法,是用來偽裝一個私有變數的,但其實作在可以用Symbol物件完成
this._bar = bar
}
Fubar()
上面代碼的 Fubar 為建構式,use strict 命令保證了該函式在嚴格模式下運行,
由于嚴格模式中,函式內部的 this 不能指向全域物件,默認等于 undefined,導致不加 new 呼叫會報錯,(JavaScript 不允許對 undefined 添加屬性)
另一個解決辦法,建構式內部判斷是否使用 new 命令,如果發現沒有使用,則直接回傳一個實體物件,
function Fubar(foo, bar) {
if (!(this instanceof Fubar)) {
return new Fubar(foo, bar)
}
this._foo = foo
this._bar = bar
}
Fubar(1, 2)._foo(
// 1
new Fubar(1, 2)
)._foo // 1
上面代碼中的建構式,不管加不加 new 命令,都會得到同樣的結果,
ES5 的語法是不是很變態?我很慶幸在 ES6 之后學習的 JS(笑),
new 命令的原理
使用 new 命令時,它后面的函式依次執行下面的步驟:
- 創建一個空物件,作為將要回傳的物件實體;
- 將這個空物件的原型,指向建構式的
prototype屬性; - 將這個空物件賦值給函式內部的
this關鍵字; - 開始執行建構式內部的代碼,
也就是說,建構式內部,this 指的是一個新生成的空物件,所有針對 this 的操作,都會發生在這個空物件上,
建構式之所以叫“建構式”,就是說這個函式的目的,就是操作一個空物件(即 this 物件),將其“構造”為需要的樣子,
如果建構式內部有 return 陳述句,而且 return 后面跟著一個物件,new 命令會回傳 return 陳述句指定的物件;否則,就會不管 return 陳述句,回傳 this 物件,
let Vehicle = function () {
this.price = 1000
return 1000
}
new Vehicle() === 1000 // false
上面代碼中,建構式 Vehicle 的 return 陳述句回傳一個數值,
這時,new 命令就會忽略這個 return 陳述句,回傳“構造”后的 this 物件,
但是,如果 return 陳述句回傳的是一個跟 this 無關的新物件,new 命令會回傳這個新物件,而不是 this 物件,這一點非常非常的詭異!
let Vehicle = function () {
this.price = 1000
return { price: 2000 }
}
new Vehicle().price
// 2000
上面代碼中,建構式 Vehicle 的 return 陳述句,回傳的是一個新物件,new 命令會回傳這個物件,而不是 this 物件,
另一方面,如果對普通函式(內部沒有 this 關鍵字的函式)使用 new 命令,則會回傳一個空物件,
function getMessage() {
return 'this is a message'
}
let msg = new getMessage()
msg // {}
typeof msg // "object"
上面代碼中,getMessage 是一個普通函式,回傳一個字串,對它使用 new 命令,會得到一個空物件,
這是因為 new 命令總是回傳一個物件,要么是實體物件,要么是 return 陳述句指定的物件,
但是在例子中,return 陳述句回傳的是字串,所以 new 命令就忽略了該陳述句,
new 命令簡化的內部流程,可以用下面的代碼表示,
function _new(constructor, params) {
let args = [].slice.call(arguments)
let constructor = args.shift()
let context = Object.create(constructor.prototype)
let result = constructor.apply(context, args)
return typeof result === 'object' && result != null ? result : context
}
// 實體
let actor = _new(Person, '張三', 28)
結論,ES6 真好!可是這些我們一定要了解,因為 ES6 只是語法糖,這個才是本質,
最后,寫一個簡單例子:
使用 ES6 的 class 方法來做一個手機類:
class Phone {
constructor(brand, price) {
this.brand = brand
this.price = price
}
call() {
return `我使用了${this.brand}手機打電話!`
}
}
let a = new Person('蘋果', '100')
使用原型方法來實作:
function phone(brand, price) {
this.brand = brand
this.price = price
}
phone.prototype.call = function () {
return `我使用了${this.brand}手機打電話!`
}
let a = new phone('華為', '200')
(完)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/168619.html
標籤:JavaScript
上一篇:webpack入門
下一篇:JS基礎語法---冒泡順序
