在学习JS的过程中,感受到其与经典OOP语言JAVA的较大不同之处。所以写了这篇文章,记录这些不同的地方。至于一些相似的特性,则没有提及。更多了解可以在MDN查阅上相关文档。
概述
对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,我们现在确实可以通过类似于Java的方式创建和继承一个“类”,但底层仍然是基于原型的,这个“类”也只是模拟出来的,实际上底层仍是一个对象。下面当我们提到类时,不要忘记这一点。JS中一切皆对象。
在传统的OOP中,我们通过class关键字定义类,然后在里面写我们的构造函数。JS中并不如此,当我们在写Ball的构造器时,如function Ball(){}
,这就已经定义好了一个Ball类,当然我们没有定义任何属性。
当我们定义好Ball的构造器function Ball(){}
,通过上面的了解我们知道,这实际上就是一个对象。在JS中,任何一个对象(除了顶层对象Object,其原型为null),都会自动有一个原型对象。Ball也有一个原型对象,从意义上来说,Ball就是继承自这个原型对象。
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__
属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
举例
当我们这样定义一个Ball类并在JS控制台中输出他的prototype属性
1 | function Ball(){} |
结果是这样的:
1 | { |
这就是Ball的原型对象。
然后创建他的实例对象并在JS控制台中输出其_proto_属性
1 | const aBall=new Ball(); |
我们会看到相同的结果。Ball的prototype
和aBall的的\_proto\_
指向的就是同一个对象。
定义我们的类
事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype
属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
1 | // 构造器及其属性定义 |
construct属性
每个实例对象都从原型中继承了一个 constructor
属性,该属性指向了用于构造此实例对象的构造函数。
例如,在控制台中尝试下面的指令:
1 | person1.constructor |
都将返回 Person() 构造器,因为该构造器包含这些实例的原始定义。 你可以在 constructor
属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。
在控制台中输入:
1 | var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']); |
现在尝试访问新建对象的属性,例如:
1 | person3.name.first |
正常工作。通常你不会去用这种方法创建新的实例;但如果你刚好因为某些原因没有原始构造器的引用,那么这种方法就很有用了,类似于JAVA的反射应用。
使用 Object.create 创建的对象
ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:
new一个对象时所发生的的事情
当你执行:
1 | var o = new Foo(); |
JavaScript 实际上执行的是:
1 | var o = new Object(); |
如何继承一个构造器
1 | //下面一段是继承某个构造器的标准流程。 |