彻底研透javascript中的对象及面向对象编程
1、什么是对象、对象的属性、方法
对象是由一些变量和函数组成的一个集合,我们将这些变量和函数称之为对象里面的属性和方法,如下是一个自定义对象单例(对象直接量)的例子:
123456789101112131415 | var car = { brand : ‘BMW‘,//品牌 model : ‘X3‘,//型号 endurance : ‘600km‘,//续航里程 oil : 30,//邮箱油量 getBrand : function() { alert(this.brand); }, getOil: function(){ alert(this.oil); }, addOil: function(n) { this.oil += n; }}; |
我们可以通过以下一些代码访问对象成员:
1234 | car.brand;//访问car的品牌car.getBrand();//输出car的品牌car.addOil(1);//添加car的油量car.getOil();//获得car的油量 |
此示例代码地址:
在javascript其实我们一直都在使用对象,当我们这样使用字符串的方法时:
1 | myString.split(‘,‘); |
当这样访问document对象时:
12 | var myDiv = document.createElement(‘div‘);var myVideo = document.querySelector(‘video‘); |
javascript还有很多内建对象,如:Array、Math、Date等。
2、this的指向
在上面car的对象定义中,我们用到了this。
this 指向了代码所在的对象(代码运行时所在的对象),在对象直接量里this看起来不是很有用,但是当你动态创建一个对象(例如使用构造器)时它是非常有用的,之后你会更清楚它的用途。关于this的更详细文章:彻底领悟javascript中的this
3、创建对象的几种方式
除了1中直接用对象直接量创建对象,我们还有以下几种方式:
- 通过构造函数创建
12345678910111213141516171819202122232425262728
function 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()构造函数
123456789101112131415161718 | var car1 = new Object();//空对象car.brand = "BMW";//成员赋值var car2 = 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; }}) |
- 通过Object对象的create()方法
通过这种方式创建的属性和方法在原型对象上,不可枚举。1234567891011121314151617
var 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__指向的即是car1var 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代码,然后按回车来运行.
1234567 | function doSomething(){}console.log( doSomething.prototype );// It does not matter how you declare the function, a// function in javascript will always have a default// prototype property.var doSomething = function(){}; console.log( doSomething.prototype ); |
正如上面所看到的, doSomething 函数有一个默认的原型属性,它在控制台上面呈现了出来. 运行这段代码之后,控制台上面应该出现了像这样的一个对象.
123456789101112 | { constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() }} |
现在,我们可以添加一些属性到 doSomething 的原型上面,如下所示.
123 | function doSomething(){}doSomething.prototype.foo = "bar";console.log( doSomething.prototype ); |
结果:
12345678910111213 | { foo: "bar", constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() }} |
然后,我们可以使用 new 运算符来在现在的这个原型基础之上,创建一个 doSomething 的实例。正确使用 new 运算符的方法就是在正常调用函数时,在函数名的前面加上一个 new 前缀. 通过这种方法,在调用函数前加一个 new ,它就会返回一个这个函数的实例化对象. 然后,就可以在这个对象上面添加一些属性.
12345 | function doSomething(){}doSomething.prototype.foo = "bar"; // add a property onto the prototypevar doSomeInstancing = new doSomething();doSomeInstancing.prop = "some value"; // add a property onto the objectconsole.log( doSomeInstancing ); |
结果:
12345678910111213141516 | { prop: "some value", __proto__: { foo: "bar", constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() } }} |
就像上面看到的, 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是怎么运行的非常有用,建议看不懂的多读几遍,好好理解一下。)
12345678910 | function doSomething(){}doSomething.prototype.foo = "bar";var doSomeInstancing = new doSomething();doSomeInstancing.prop = "some value";console.log("doSomeInstancing.prop: " + doSomeInstancing.prop);console.log("doSomeInstancing.foo: " + doSomeInstancing.foo);console.log("doSomething.prop: " + doSomething.prop);console.log("doSomething.foo: " + doSomething.foo);console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);console.log("doSomething.prototype.foo: " + doSomething.prototype.foo); |
结果:
123456 | doSomeInstancing.prop: some valuedoSomeInstancing.foo: bardoSomething.prop: undefineddoSomething.foo: undefineddoSomething.prototype.prop: undefineddoSomething.prototype.foo: bar |
修改原型:
我们从下面这个例子来看一下如何修改构造器的 prototype 属性。
在已有的Car构造函数的定义后面,增加以下这段代码,它将为构造器的 prototype 属性添加一个新的方法:
123456 | function Car(){ //......};Car.prototype.setPrice = function(price){ this.price = ‘10万‘;} |
这样定义后,所有Car的实例对象都具有了setPrice()这个方法,包括在这个方法定义之前创建的实例对象(这就是前面讲的原型链的原理)。
我们一般通过prototype添加方法,不推荐使用它来添加属性。
事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
123456789101112131415 | // 构造器及其属性定义function Test(a,b,c,d) { // 属性定义};// 定义第一个方法Test.prototype.x = function () { ... }// 定义第二个方法Test.prototype.y = function () { ... }// 等等…… |
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。
1234567891011121314151617181920212223 | "use strict";class Polygon { constructor(height, width) { this.height = height; this.width = width; }}class Square extends Polygon { constructor(sideLength) { super(sideLength, sideLength); } get area() { return this.height * this.width; } set sideLength(newLength) { this.height = newLength; this.width = newLength; }}var square = new Square(2); |
Class的取值函数(getter)和存值函数(setter):
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
12345678910111213141516171819 | class MyClass { constructor() { // ... } get prop() { return ‘getter‘; } set prop(value) { console.log(‘setter: ‘+value); }}let inst = new MyClass();inst.prop = 123;// setter: 123inst.prop// ‘getter‘ |
上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
Class 的静态方法:
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
1234567891011 | class Foo { static classMethod() { return ‘hello‘; }}Foo.classMethod() // ‘hello‘var foo = new Foo();foo.classMethod()// TypeError: foo.classMethod is not a function |
上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
父类的静态方法,可以被子类继承。
12345678910 | class Foo { static classMethod() { return ‘hello‘; }}class Bar extends Foo {}Bar.classMethod(); // ‘hello‘ |
上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。
静态方法也是可以从super对象上调用的。
12345678910111213 | class Foo { static classMethod() { return ‘hello‘; }}class Bar extends Foo { static classMethod() { return super.classMethod() + ‘, too‘; }}Bar.classMethod(); |
Class的静态属性和实例属性:
静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。
12345 | class Foo {}Foo.prop = 1;Foo.prop // 1 |
目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。
12345678910 | // 以下两种写法都无效class Foo { // 写法一 prop: 2 // 写法二 static prop: 2}Foo.prop // undefined |
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内部加上代码。
12345678910 | class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ‘ ‘ + super.toString(); // 调用父类的toString() }} |
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
12345678 | class Point { /* ... */ }class ColorPoint extends Point { constructor() { }}let cp = new ColorPoint(); // ReferenceError |
上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
123 | constructor(...args) { super(...args);} |
另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
1234567891011121314 | class Point { constructor(x, y) { this.x = x; this.y = y; }}class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); this.color = color; // 正确 }} |
上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。
下面是生成子类实例的代码。
1234 | let cp = new ColorPoint(25, 8, ‘green‘);cp instanceof ColorPoint // truecp instanceof Point // true |
上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。