侧边栏

JavaScript中的面向对象

发布于 | 分类于 编程语言/JavaScript

JS中的面向对象与C++中的面向对象有很大不同。由于不存在类的概念,在初学JS的面向对象时略感困惑。

《JS高级程序设计中》关于这一块讲的十分精彩,先来整理一下关于JS创建对象的方法,然后研究JS中的原型继承的一些细节知识点。

参考

创建对象

首先来看看几种创建普通对象的方式

对象字面量

可以使用对象字面量来创建一个对象。

js
var bird = {
    name: "crow",
    age:3,
    fly: function(){
        alert("I'm "+this.name+", I'm flying "+this.age+" years!");
    }
}
bird.fly();
  • 优点:直接明了,相当于只是创建了一个引用数据类型的变量;
  • 缺点:只限于一个对象,当使用同一个接口创建多个对象时,会产生大量重复的代码;

工厂模式

可以创建一个函数,并在其中创建一个空对象,将需要的属性值作为参数赋给对象属性,并将获得值之后的属性返回。

js
function createBird(name,age){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.fly = function(){
        alert("I'm "+this.name+", I'm flying "+this.age+" years!");
    }
    return o;
}
var bird = createBird("crow", 3);
bird.fly();
  • 优点:抽象了创建具体函数的过程,可以创建多个相似的对象;
  • 缺点:无法知道对象的类型(只能是Object)。

构造函数

构造函数与工厂模式类似,只是在其外部采用new调用,在其内部采用this代替显式创建空对象。构造函数名称首字母一般大写。

js
function Bird(name,age){
    this.name = name;
    this.age = age;
    this.fly = function(){
        alert("I'm "+this.name+", I'm flying "+this.age+" years!");
    }
}

var bird = new Bird("crow", 3);
bird.fly();
  • 优点:可以识别实例对象的自定义类型(即构造函数名)。
  • 缺点:所有的实例对象无法共享相同的属性和方法,造成内存的浪费。
js
var bird = new Bird("crow", 3);
var bird2 = new Bird("mmm", 1);

console.log(bird.fly === bird.fly2) // 连同一个函数都无法共享

原型

原型prototype是JS中中一个非常重要的属性,接下来重点理清这个问题。

首先需要明白,无论什么时候,只要新建了一个函数,就会为该函数创建一个prototype属性,这个属性指向了函数的原型对象;

其次,JS中的所有对象都有一个特殊的属性__proto__,也指向其构造函数的原型对象。

每个原型对象都有一个constructor属性,这个属性是一个指向prototype属性所在函数的指针

js
var bird = new Bird("crow", 3)

bird.__proto__ === Bird.prototype // true

Bird.prototype // 一个特殊的对象
Bird.prototype.constructor === Bird  // true

OK,现在明白了,构造函数的prototype是一个对象,成为构造函数的原型对象。

那么这个原型对象有什么用呢?

当自定了一个构造函数时,这个构造函数的原型对象默认只会获取到constructor属性,并继承Object的属性和方法(简单来说,就是刚开始原型对象上面除了constructor之外也没啥有用的属性和方法)。

既然原型是一个对象,我们就可以向这个对象上面添加属性和方法

js
Bird.prototype.x = 1
Bird.prototype.eat = function() {
    console.log('bird eat')
}

神奇的事情即将发生。当为原型对象添加了这些属性和方法之后,通过Bird构造函数创建的变量,也可以访问这些属性和方法!

js
console.log(bird.x)
bird.eat()

共享静态属性和方法

在前面的Bird构造函数中,我们是在函数内定义了对象的属性和方法,这导致通过同一个构造函数创建的对象,都有自己的属性和方法,无法共享属性,造成了内存的浪费。

这也是原型的第一个作用:统一保存将多个对象共享的属性和方法

js
bird.eat === bird2.eat // true,因此他们都是保存在同一个原型对象上面的方法

JS中常见的构造函数一般按照下面的写法:在构造函数中定义的实例属性,在原型上定义方法与静态属性。

js
function Bird(name,age){
    this.name = name;
    this.age = age;
    this.home = ["tree","grass"]
}
Bird.prototype = {
    showhome: function(){
        alert(this.home);
    },
    fly: function(){
        alert("I'm "+this.name+", I'm flying "+this.age+" years!");
    }
}

var bird1 = new Bird("crow", 3)
var bird2 = new Bird("duck", 5)
bird1.fly() //crow
bird2.name = "swan"
bird2.fly() //duck

bird1.home[0] = "appleTree"
bird2.showhome() //tree,grass

在JS中,当访问一个对象的属性或方法时,先在对象自己的属性方法中查找,如果没有,再沿着__proto__去找他原型对象上的属性和方法。

可以通过hasOwnProperty来判断一个属性到底是实例对象的还是其原型的。

js
console.log(bird.hasOwnProperty('name')) // true
console.log(bird.hasOwnProperty('x')) // false

等等,上面的这个hasOwnProperty属性并没有在Bird或者Bird.prototype中定义,那么它是怎么被找到呢?

关于这一点,我们在下面的继承原型链中将会提到。

继承

JS也是一门面向对象OOP的语言,也具备面向对象封装、继承、多态等特性。

上面通过构造函数和原型,演示了封装的过程,接下来看看JS中的继承

原型链

既然原型是一个对像,那么我们可以直接将函数的原型对象替换为另外一个对象吗?

js
Bird.prototype = someObj

完全可以!,这跟我们手动向Bird.prototype添加属性和方法没有什么区别。

因此,只需要修改构造函数的原型对象,就可以实现继承。

js

function Animal(type){
    this.type = type
}
Animal.prototype.walk = function(){
    console.log('walk')
}

Bird.prototype = new Animal("bird"); //替换Bird构造函数的原型对象

var bird = new Bird("crow", 3);

bird.type  // 父对象的属性
bird.walk() // 父对象的方法

bird.age // 子对象自己的属性

通过原型,就可以将公共的属性和方法一直继承下去。这种像链表节点一样的,在JS中被称作原型链。

在JS中,当访问一个对象的属性或方法时,先在对象自己的属性方法中查找,如果没有,再沿着__proto__去找他原型对象上的属性和方法

回到前面的bird.hasOwnProperty这个方法,所有对象都有一个特殊的属性__proto__。因此当在Bird.prototype上找不到hasOwnProperty方法时,就会向Bird.prototype.__proto__继续查找

Bird.prototype.__proto__对象的最原始构造函数都是Object(实际上所有对象的原始构造函数都是Object),其原型Object.prototype上就定义了hasOwnProperty,这就是为什么bird对象也可以使用这个方法的原因。

js
bird.__proto__ == Bird.prototype

Bird.prototype.__proto__ = Object.prototype //  Object.prototype定义了基础的hasOwnProperty、toString等方法

可以简单理解为Object.prototype是JS中的第一个对象。因此,原型链的查询终止条件就是

js
Object.prototype.__proto__ == null

可以通过使用instanceof来判断两个类是否存在继承关系,其规则为:

  • 沿着左操作数的__proto__这条线来找,同时沿着右操作数的prototype这条线来找
  • 如果两条线能找到同一个引用,即同一个对象,那么就返回true
  • 如果找到终点还未重合,则返回false

所有对象的构造函数都是Object,所有函数的构造函数都是Function,而在JS中,函数也是一种特殊的对象,因此,当看见下面的表达式结果时,不要慌张

js
console.log(Object instanceof Function); // true 
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true

从原型链上来解释一下

js
Object.__proto__ == Function.prototype; // Object()函数的构造函数也是Function()

Function.__proto__ == Function.prototype; // Function()函数是被自身创建的-_-

Function.prototype.__proto__ == Object.prototype; // Function()的原型也是对象

注意事项

可以通过搜索查询原型对象中的值(包括属性和方法),却无法通过对象实例修改原型对象中的值;

当为实例对象添加了一个属性,如果原型对象中存在同名属性,则会被实例对象的属性所覆盖;

如果原型对象中不存在该同名属性,该属性也只是该实例对象所独有的,并不能被原型对象下的其他实例对象所共享;

在添加的属性之后又希望重新访问原型对象中同名属性的值,可以使用delete删除实例对象中覆盖的属性并恢复对于原型对象中属性的访问。

每次使用Bird.prototype.prototype就显得十分麻烦,更常见的做法是采用一个包含所有属性和方法的对象字面量来重写整个原型对象,即

js
Bird.prototype = {...};

这么做相当于重写了这个Bird.prototype,并且其constructor属性会指向Object函数而不是Bird,解决这个问题的办法是手动修正。

js
Bird.prototype = {...}
// 根据实际情况看看是否需要恢复原型对象的constructor属性
Bird.prototype.constructor = Bird

由于对实例对象的属性查询是一个搜索过程,且每次查询都会执行该搜索过程,因此可以在程序中随时为原型对象增加属性和方法,并在之后的代码中实例对象中访问这些共享的属性和方法,即使这个实例对象先于新增属性而创建

js
function Bird() {
}
Bird.prototype.x = 0

var bird = new Bird();

Bird.prototype.x = 1 // 有对象创建之后重新修改原型上的属性

console.log(bird.x) // 输出 1

尽管可以随时为原型添加属性和方法,并在修改之后可以被所有的实例对象所访问;但是如果重写整个原型对象(比如采用对象字面量赋值的方式),会切断前面创建的实例对象与原型对象之间的联系

js
function Bird() {

}
Bird.prototype = { x: 1 }
var bird = new Bird()
Bird.prototype = { x: 2 }

console.log(bird.x) // bird.__proto__指向的是之前的原型对象,{x:1}

var bird2 = new Bird()
console.log(bird2.x) //  bird2.__proto__指向的是之前的原型对象,{x:2}

小结

JS跟传统的面向对象语言有很大的区别,从上面的原理分析中可以看出,JS中貌似根本就没有“类”的存在,构造函数只是一个普通的函数,继承的属性和方法,不是放在类上面,而是放在原型对象上面。

原型也是一个对象,通过__proto__构建出原型链,实现继承的特性,学习JS,需要牢牢掌握这一点。

除了继承之外,JS中的this也跟一般的语言不通,接下来会了解构造函数中this的用法。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。