彻底研透javascript中的对象及面向对象编程
1、什么是对象、对象的属性、方法
对象是由一些变量和函数组成的一个集合,我们将这些变量和函数称之为对象里面的属性和方法,如下是一个自定义对象单例(对象直接量)的例子:
1
|
var car = {
|
我们可以通过以下一些代码访问对象成员:
1
|
car.brand;//访问car的品牌
|
此示例代码地址:
在javascript其实我们一直都在使用对象,当我们这样使用字符串的方法时:
1
|
myString.split(',');
|
当这样访问document对象时:
1
|
var myDiv = document.createElement('div');
|
javascript还有很多内建对象,如:Array、Math、Date等。
2、this的指向
在上面car的对象定义中,我们用到了this。
this 指向了代码所在的对象(代码运行时所在的对象),在对象直接量里this看起来不是很有用,但是当你动态创建一个对象(例如使用构造器)时它是非常有用的,之后你会更清楚它的用途。关于this的更详细文章:彻底领悟javascript中的this
3、创建对象的几种方式
除了1中直接用对象直接量创建对象,我们还有以下几种方式:
- 通过构造函数创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28function Car(params) {
this.brand=params.brand;//品牌
this.model=params.model;//型号
this.endurance=params.endurance;//续航里程
this.oil=params.oil;//邮箱油量
this.getBrand=function() {
alert(this.brand);
},
this.getOil=function(){
alert(this.oil);
},
this.addOil=function(n) {
this.oil += n;
}
}
var Car1 = new Car({
brand : 'BMW',//品牌
model : 'X3',//型号
endurance : '600km',//续航里程
oil : 30,//邮箱油量
});
var Car2 = new Car({
brand : 'BYD',//品牌
model : '元',//型号
endurance : '500km',//续航里程
oil : 50,//邮箱油量
});
- 通过Object()构造函数
1
|
var car1 = new Object();//空对象
|
- 通过Object对象的create()方法
通过这种方式创建的属性和方法在原型对象上,不可枚举。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var car1 = new Object({//构造时就填充属性和方法
brand : 'BMW',//品牌
model : 'X3',//型号
endurance : '600km',//续航里程
oil : 30,//邮箱油量
getBrand : function() {
alert(this.brand);
},
getOil: function(){
alert(this.oil);
},
addOil: function(n) {
this.oil += n;
}
});
//以 car1 为原型对象创建了 car2 对象。car2.__proto__指向的即是car1
var car2 = Object.create(car1);//以car1为基础创建car2,它们具有相同的属性和方法,但属于不同的引用,创建的属性和方法在原型对象上,不可枚举
4、原型对象(prototype)
每个对象拥有一个原型对象(prototype),对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。因为prototype是函数的一个特殊属性,而不是对象的。
在传统的 OOP 中(如:java),首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是proto属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
Object.getPrototypeOf(new Foobar())和Foobar.prototype指向着同一个对象,都是指向Foobar构造函数的prototype。
在javascript中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型(prototype) 。打开一个控制台 (在Chrome和Firefox中,可以按Ctrl+Shift+I来打开)切换到”控制台” 选项卡, 复制粘贴下面的JavaScript代码,然后按回车来运行.
1
|
function doSomething(){}
|
正如上面所看到的, doSomething 函数有一个默认的原型属性,它在控制台上面呈现了出来. 运行这段代码之后,控制台上面应该出现了像这样的一个对象.
1
|
{
|
现在,我们可以添加一些属性到 doSomething 的原型上面,如下所示.
1
|
function doSomething(){}
|
结果:
1
|
{
|
然后,我们可以使用 new 运算符来在现在的这个原型基础之上,创建一个 doSomething 的实例。正确使用 new 运算符的方法就是在正常调用函数时,在函数名的前面加上一个 new 前缀. 通过这种方法,在调用函数前加一个 new ,它就会返回一个这个函数的实例化对象. 然后,就可以在这个对象上面添加一些属性.
1
|
function doSomething(){}
|
结果:
1
|
{
|
就像上面看到的, doSomeInstancing 的 proto 属性就是doSomething.prototype. 但是这又有什么用呢? 好吧,当你访问 doSomeInstancing 的一个属性, 浏览器首先查找 doSomeInstancing 是否有这个属性. 如果 doSomeInstancing 没有这个属性, 然后浏览器就会在 doSomeInstancing 的 proto 中查找这个属性(也就是 doSomething.prototype). 如果 doSomeInstancing 的 proto 有这个属性, 那么 doSomeInstancing 的 proto 上的这个属性就会被使用. 否则, 如果 doSomeInstancing 的 proto 没有这个属性, 浏览器就会去查找 doSomeInstancing 的 proto 的 proto ,看它是否有这个属性. 默认情况下, 所有函数的原型属性的 proto 就是 window.Object.prototype. 所以 doSomeInstancing 的 proto 的 proto (也就是 doSomething.prototype 的 proto (也就是 Object.prototype)) 会被查找是否有这个属性. 如果没有在它里面找到这个属性, 然后就会在 doSomeInstancing 的 proto 的 proto 的 proto 里面查找. 然而这有一个问题: doSomeInstancing 的 proto 的 proto 的 proto 不存在. 最后, 原型链上面的所有的 proto 都被找完了, 浏览器所有已经声明了的 proto 上都不存在这个属性,然后就得出结论,这个属性是 undefined.(这段很拗口,但是对理解prototype是怎么运行的非常有用,建议看不懂的多读几遍,好好理解一下。)
1
|
function doSomething(){}
|
结果:
1
|
doSomeInstancing.prop: some value
|
修改原型:
我们从下面这个例子来看一下如何修改构造器的 prototype 属性。
在已有的Car构造函数的定义后面,增加以下这段代码,它将为构造器的 prototype 属性添加一个新的方法:
1
|
function Car(){
|
这样定义后,所有Car的实例对象都具有了setPrice()这个方法,包括在这个方法定义之前创建的实例对象(这就是前面讲的原型链的原理)。
我们一般通过prototype添加方法,不推荐使用它来添加属性。
事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
1
|
// 构造器及其属性定义
|
5.proto 中的constructor 属性
每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。某些情况下,如果我们找不到某个对象的构造函数的引用,又希望能继续创建一个同类型的对象,我们可以使用该对象中的_proto_中的constructor属性来创建,假如有一个对象car1,那么我们可以用如下的方式创建car2
1
|
var car2 = new car1.constructor();//注意:_proto_中的属性都可以通过实例对象直接访问
|
除此之外,我们还可以通过constructor属性获得某个对象实例的构造器的名字,如下:
1
|
var constructorName = car1.constructor.name;//constructor的name属性为构造器的名字
|
6、ES6中Class关键字定义类
ECMAScript6 引入了一套新的关键字用来实现 class。ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。这些新的关键字包括 class, constructor,static,extends 和 super。
1
|
|
Class的取值函数(getter)和存值函数(setter):
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
1
|
class MyClass {
|
上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
Class 的静态方法:
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
1
|
class Foo {
|
上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
父类的静态方法,可以被子类继承。
1
|
class Foo {
|
上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。
静态方法也是可以从super对象上调用的。
1
|
class Foo {
|
Class的静态属性和实例属性:
静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。
1
|
class Foo {
|
目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。
1
|
// 以下两种写法都无效
|
7、javascript中的继承
- ES5的通过修改原型链实现继承
-
function superClass(){ this.value = “super”; } superClass.prototype.getSuperValue = function(){ return this.value; } function subClass(){ this.subClassValue = “sub”; } subClass.prototype = new superClass(); subClass.prototype.getSubValue = function(){ return this.subClassValue; } var s = new subClass(); alert(s.getSuperValue());
subClass.prototype = new superClass(); subClass.prototype 指向superClass的实例意味着什么呢,意味着
subClass.prototype指向了superClass的prototype,所以就能访问到原型中的属性和方法。
————————————————
版权声明:本文为CSDN博主「非著名coder」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/houyaowei/java/article/details/51444145
- ECMAScript6中Class的继承
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
1
|
class ColorPoint extends Point {}
|
上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。
1
|
class ColorPoint extends Point {
|
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
1
|
class Point { /* ... */ }
|
上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
1
|
constructor(...args) {
|
另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
1
|
class Point {
|
上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。
下面是生成子类实例的代码。
1
|
let cp = new ColorPoint(25, 8, 'green');
|
上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。