前言
JavaScript 程序员一定都知道函数是作为一种对象来使用的,也就是说,函数可以像其他对象一样被拓展和修改。其中最重要的一个特性就是原型链(prototype chain)。
在 ECMAScript 2018 中,原型链相关的概念被重新整理和解释,它们将会影响我们如何编写代码、如何设计类库,以及其他一些重要的方面。本文将会探讨 ECMAScript 2018 中的原型链改变技巧及利弊分析。
原型链
首先,我们需要对原型链进行一个基本的介绍。
在 JavaScript 中,每个对象都拥有一个原型对象,也称为 "proto"。原型对象本身也是一个对象,并且它也拥有一个原型对象。这些原型对象组成了一条链,我们称其为原型链。
我们可以通过 Object.getPrototypeOf()
方法来访问一个对象的原型,例如:
const obj = { foo: 'bar' }; console.log(Object.getPrototypeOf(obj)); // 输出 {}
在这个例子中,const obj = { foo: 'bar' }
创建了一个对象,它的原型为 {}
(一个空对象)。
如果我们打印其中的 foo
属性,则会返回 bar
:
console.log(obj.foo); // 输出 'bar'
这是因为 JavaScript 引擎在读取 obj.foo 的时候,会在 obj 中查找 foo
属性。但如果在 obj 中没有找到,就会去 obj 的原型 {}
中查找,这样一直查找下去,直到 Object.prototype 这个顶级对象(也就是原型链的根)为止。如果还是没有找到,就会返回 undefined
。
利弊分析
但是,原型链也带来了一些问题。其中最常见的问题是,如果我们修改了一个对象的原型,所有基于该原型的对象都会受到影响。
例如:
-- -------------------- ---- ------- ----- ------ - - ------ - ------------------------- - -- ----- --- - - ------ - ------------------------- - -- -------------------------- -------- ----- --- - - ------ - ------------------------- - -- -------------------------- -------- ----------- -- -- ------------ ----------- -- -- ------------
在这个例子中,我们创建了一个 animal
对象,它有一个 walk
方法,用于输出 "Walking..."。我们还定义了一个 dog
对象,它有一个 bark
方法,用于输出 "Barking..."。dog
的原型为 animal
。我们还定义了一个 cat
对象,它有一个 meow
方法,也把 animal
作为原型。
如果我们在 animal
中添加了一个新方法,我们会发现 dog
和 cat
也会受到影响:
animal.run = function() { console.log('Running...') }; dog.run(); // 输出 'Running...' cat.run(); // 输出 'Running...'
这是因为它们的原型链中都有 animal
。
这种特性常常导致一些难以调试的错误。对于普通开发者来说,建议不要动态修改原型,以避免产生类似的问题。
Object.setPrototypeOf()
在 ECMAScript 5 中,我们可以使用 Object.create()
方法来创建一个具有指定原型的新对象。在 ECMAScript 6 中,我们还可以使用 class
关键字来创建类,它也支持基于原型的继承。那么在 ECMAScript 2018 中,我们有什么新的方法可以用来修改原型链吗?
在 ECMAScript 2018 中,我们可以使用 Object.setPrototypeOf()
方法来修改一个对象的原型。
例如:
-- -------------------- ---- ------- ----- ------ - - ------ - ------------------------- - -- ----- --- - - ------ - ------------------------- - -- ----- --- - - ------ - ------------------------- - -- -------------------------- -------- -------------------------- -------- ----------- -- -- ------------ ----------- -- -- ------------ ----------------------------- - ----- - ------------------------- - --- ---------- -- -- ------------ ---------- -- -- ------------
在这个例子中,我们先创建了一个 animal
对象,表示动物类。它有一个 walk
方法。
然后,我们创建了 dog
和 cat
两个对象,dog
有一个 bark
方法,cat
有一个 meow
方法。
通过 Object.setPrototypeOf(dog, animal)
和 Object.setPrototypeOf(cat, animal)
把 animal
设置为它们的原型。
最后,我们使用 Object.setPrototypeOf(animal, { run() { console.log('Running...') } })
将 animal
的原型修改为一个新对象,表示动物可以奔跑。
通过这个操作,我们发现,dog
和 cat
上的方法都已经发生了变化,它们新增了一个 run
方法。
利弊分析
虽然 Object.setPrototypeOf()
提供了一个很方便的方法来改变对象的原型,但同样也存在一些风险。因为原型链是全局的,这个方法有可能会影响到整个应用程序。因此,在使用这个方法的时候一定要非常小心。
另外,由于 Object.setPrototypeOf()
会影响到整个原型链,当你大量使用它时,你的代码可能变得难以维护。
Object.assign()
虽然 Object.setPrototypeOf()
使得修改原型变得更加容易,但它同样带来了风险。而且,它需要实时改变原型,这可能会影响性能。
还有一种更加安全和高效的方法来实现类似的功能,那就是 Object.assign()
。
例如:
-- -------------------- ---- ------- ----- ------ - - ------ - ------------------------- - -- ----- --- - - ------ - ------------------------- - -- ----- --- - - ------ - ------------------------- - -- ------------------ -------- ------------------ -------- ----------- -- -- ------------ ----------- -- -- ------------ --------------------- - ----- - ------------------------- - --- ---------- -- -- ------------ ---------- -- -- ------------
在这个例子中,我们使用 Object.assign()
将 animal
合并到 dog
和 cat
中。
与 Object.setPrototypeOf()
不同,Object.assign()
并没有直接改变对象原型。它仅仅将 animal
中的方法和属性复制到了 dog
和 cat
中。后续修改原型的操作也不会影响到 dog
和 cat
。
利弊分析
使用 Object.assign()
版本的代码更加安全和高效。但它有一个限制,就是它只能用来合并方法和属性,不能用来修改原型链。如果你需要改变原型链,还是需要使用 Object.setPrototypeOf()
。
应用
让我们来看一个更具实际应用的例子,它演示了如何使用 Object.assign()
来实现基于原型的类继承。
我们有一个 Mammal
类,它是所有哺乳动物的基类。我们还有一个 Dog
类,它从 Mammal
继承,并添加了一些狗特有的方法。
-- -------------------- ---- ------- ----- ------ - ----------------- - --------- - ----- - ------ - ------------------------- -- ------------- - - ----- --- ------- ------ - ----------------- ------ - ------------ ---------- - ------ - ------ - -------------------------- ------------ -- ------------- - -
现在,我们想要创建一个 Cat
类,也从 Mammal
继承。我们可以重用 Dog
类的定义,并将 Dog
中的特定属性和方法替换为 Cat
中的。
首先,让我们看看如何使用 Object.assign()
来复制 Dog.prototype
到 Cat.prototype
中:
class Cat extends Mammal { constructor(name, breed) { super(name); this.breed = breed; } } Object.assign(Cat.prototype, Dog.prototype);
然后,我们再从 Cat
的原型中删除 bark
方法,以使它只继承 Mammal
中的 walk
方法:
delete Cat.prototype.bark;
完整的代码如下:
-- -------------------- ---- ------- ----- ------ - ----------------- - --------- - ----- - ------ - ------------------------- -- ------------- - - ----- --- ------- ------ - ----------------- ------ - ------------ ---------- - ------ - ------ - -------------------------- ------------ -- ------------- - - ----- --- ------- ------ - ----------------- ------ - ------------ ---------- - ------ - - ---------------------------- --------------- ------ ------------------- ----- --- - --- ------------ --------- ------- ----------- -- -- ------ -- -----------
在这个例子中,我们通过复制 Dog.prototype
到 Cat.prototype
中,并从 Cat
的原型中删除了 bark
方法,来实现了基于原型的继承。
结论
在 ECMAScript 2018 中,我们可以使用 Object.setPrototypeOf()
或 Object.assign()
来改变对象的原型。虽然这些方法可以让我们更加便捷地修改原型链,但我们一定要非常小心,并避免滥用。修改原型链可能会对全局应用程序产生影响。
当我们需要实现类继承时,可以使用基于原型的继承来实现,例如通过将 Dog.prototype
复制到 Cat.prototype
中,并从中删除特定的方法和属性来实现。这样会更加安全和高效,并且不会影响到全局应用程序。
希望通过本文,能够帮助读者更加了解 ECMAScript 2018 中的原型链改变技巧及利弊分析,并提供一些指导意义。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/675157e88bd460d3ad88b249