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

xiaoge00 2020-05-10

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的行为完全一致。

相关推荐