彻底研透javascript中的对象及面向对象编程

Posted 前端客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了彻底研透javascript中的对象及面向对象编程相关的知识,希望对你有一定的参考价值。

1、什么是对象、对象的属性、方法

对象是由一些变量和函数组成的一个集合,我们将这些变量和函数称之为对象里面的属性和方法,如下是一个自定义对象单例(对象直接量)的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
};

我们可以通过以下一些代码访问对象成员:

1
2
3
4
car.brand;//访问car的品牌
car.getBrand();//输出car的品牌
car.addOil(1);//添加car的油量
car.getOil();//获得car的油量

此示例代码地址:

javascript其实我们一直都在使用对象,当我们这样使用字符串的方法时:

1
myString.split(\',\');

当这样访问document对象时:

1
2
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中直接用对象直接量创建对象,我们还有以下几种方式:

  • 通过构造函数创建
    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
    28
    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()构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()方法
    通过这种方式创建的属性和方法在原型对象上,不可枚举。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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__指向的即是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
2
3
4
5
6
7
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 函数有一个默认的原型属性,它在控制台上面呈现了出来. 运行这段代码之后,控制台上面应该出现了像这样的一个对象.

1
2
3
4
5
6
7
8
9
10
11
12
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}

现在,我们可以添加一些属性到 doSomething 的原型上面,如下所示.

1
2
3
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}

然后,我们可以使用 new 运算符来在现在的这个原型基础之上,创建一个 doSomething 的实例。正确使用 new 运算符的方法就是在正常调用函数时,在函数名的前面加上一个 new 前缀. 通过这种方法,在调用函数前加一个 new ,它就会返回一个这个函数的实例化对象. 然后,就可以在这个对象上面添加一些属性.

1
2
3
4
5
function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
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是怎么运行的非常有用,建议看不懂的多读几遍,好好理解一下。)

1
2
3
4
5
6
7
8
9
10
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);

结果:

1
2
3
4
5
6
doSomeInstancing.prop:      some value
doSomeInstancing.foo: bar
doSomething.prop: undefined
doSomething.foo: undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo: bar

修改原型:
我们从下面这个例子来看一下如何修改构造器的 prototype 属性。
在已有的Car构造函数的定义后面,增加以下这段代码,它将为构造器的 prototype 属性添加一个新的方法:

1
2
3
4
5
6
function Car(){
//......
};
Car.prototype.setPrice = function(price){
this.price = \'10万\';
}

这样定义后,所有Car的实例对象都具有了setPrice()这个方法,包括在这个方法定义之前创建的实例对象(这就是前面讲的原型链的原理)。
我们一般通过prototype添加方法,不推荐使用它来添加属性。
事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 构造器及其属性定义

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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"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关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
constructor() {
// ...
}
get prop() {
return \'getter\';
}
set prop(value) {
console.log(\'setter: \'+value);
}
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// \'getter\'

上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

Class 的静态方法:

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

1
2
3
4
5
6
7
8
9
10
11
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类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

父类的静态方法,可以被子类继承。

1
2
3
4
5
6
7
8
9
10
class Foo {
static classMethod() {
return \'hello\';
}
}

class Bar extends Foo {
}

Bar.classMethod(); // \'hello\'

上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

静态方法也是可以从super对象上调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo {
static classMethod() {
return \'hello\';
}
}

class Bar extends Foo {
static classMethod() {
return super.classMethod() + \', too\';
}
}

Bar.classMethod();

Class的静态属性和实例属性:
静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。

1
2
3
4
5
class Foo {
}

Foo.prop = 1;
Foo.prop // 1

目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。

1
2
3
4
5
6
7
8
9
10
// 以下两种写法都无效
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内部加上代码。

1
2
3
4
5
6
7
8
9
10
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对象。

1
2
3
4
5
6
7
8
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方法。

1
2
3
constructor(...args) {
super(...args);
}

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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方法之后就是正确的。

下面是生成子类实例的代码。

1
2
3
4
let cp = new ColorPoint(25, 8, \'green\');

cp instanceof ColorPoint // true
cp instanceof Point // true

上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。

以上是关于彻底研透javascript中的对象及面向对象编程的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript中的面向对象编程,详解原型对象及prototype,constructor,proto,内含面向对象编程详细案例(烟花案例)

掌握面向对象编程本质,彻底掌握OOP

JavaScript对象及初识面向对象

js原型及原型链

从冰箱装大象到女娲造人,带你彻底吃透Python面向对象编程

前端开发:理解JavaScript 中的面向对象编程