Top
Fork me on Gitee My Github

JavaScript 面向对象

  • 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。

  • 对象有状态:对象具有状态,同一对象可能处于不同状态之下。

  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁。

var o1 = { a: 1 };
var o2 = { a: 1 };
console.log(o1 == o2); // false

关于对象的第二个和第三个特征“状态和行为”,不同语言会使用不同的术语来抽象描述它们,比如C++中称它们为“成员变量”和“成员函数”Java中则称它们为“属性”和“方法”

在 JavaScript中,将状态和行为统一抽象为“属性”,考虑到 JavaScript 中将函数设计成一种特殊对象,所以 JavaScript中的行为和状态都能用属性来抽象。

var o = { 
   d: 1,
   f() {
        console.log(this.d);
    }    
};

所以,总结一句话来看,在JavaScript中,对象的状态和行为其实都被抽象为了属性。如果你用过Java,一定不要觉得奇怪,尽管设计思路有一定差别,但是二者都很好地表现了对象的基本特征:标识性、状态和行为。

JavaScript创始人Brendan Eich(布兰登·艾奇)在“原型运行时”的基础上引入了new、this等语言特性,使之“看起来语法更像Java”,而Java正是基于类的面向对象的代表语言之一。

什么是原型?

原型是顺应人类自然思维的产物。中文中有个成语叫做“照猫画虎”,这里的猫看起来就是虎的原型,所以,由此我们可以看出,用原型来描述对象的方法可以说是古已有之。

在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象。

最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java等流行的编程语言。这个流派叫做基于类的编程语言。

还有一种就是基于原型的编程语言,它们利用原型来描述对象。我们的JavaScript就是其中代表

“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中,总是先有类,再从类去实例化一个对象。类与类之间又可能会形成继承、组合等关系。类又往往与语言的类型系统整合,形成一定编译时的能力。

与此相对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将它们分成类。

基于原型的面向对象系统通过“复制”的方式来创建新对象。一些语言的实现中,还允许复制一个空对象。这实际上就是创建一个全新的对象。

基于原型和基于类都能够满足基本的复用和抽象需求,但是适用的场景不太相同。

这就像专业人士可能喜欢在看到老虎的时候,喜欢用猫科豹属豹亚种来描述它,但是对一些不那么正式的场合,“大猫”可能更为接近直观的感受一些(插播一个冷知识:比起老虎来,美洲狮在历史上相当长时间都被划分为猫科猫属,所以性格也跟猫更相似,比较亲人)。

我们的JavaScript 并非第一个使用原型的语言,在它之前,self、kevo等语言已经开始使用原型来描述对象了。

事实上,Brendan更是曾透露过,他最初的构想是一个拥有基于原型的面向对象能力的scheme语言。

原型系统的“复制操作”有两种实现思路:

一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用;

另一个是切实地复制对象,从此两个对象再无关联。

历史上的基于原型语言因此产生了两个流派,显然,JavaScript显然选择了前一种方式。

JavaScript的原型

如果我们抛开JavaScript用于模拟Java类的复杂语法设施(如new、Function Object、函数的prototype属性等),原型系统可以说相当简单,我可以用两条概括:

如果所有对象都有私有字段[[prototype]],就是对象的原型;
读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
这个模型在ES的各个历史版本中并没有很大改变,但从 ES6 以来,JavaScript提供了一系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:

Object.create 根据指定的原型创建新对象,原型可以是null;
Object.getPrototypeOf 获得一个对象的原型;
Object.setPrototypeOf 设置一个对象的原型。

利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。我用下面的代码展示了用原型来抽象猫和虎的例子。

var cat = {
    say(){
        console.log("meow~");
    },
    jump(){
        console.log("jump");
    }
}

var tiger = Object.create(cat,  {
    say:{
        writable:true,
        configurable:true,
        enumerable:true,
        value:function(){
            console.log("roar!");
        }
    }
})


var anotherCat = Object.create(cat);

anotherCat.say();

var anotherTiger = Object.create(tiger);

这段代码创建了一个“猫”对象,又根据猫做了一些修改创建了虎,之后我们完全可以用Object.create来创建另外的猫和虎对象,我们可以通过“原始猫对象”和“原始虎对象”来控制所有猫和虎的行为。

new 运算接受一个构造器和一组调用参数,实际上做了几件事:

以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
将 this 和调用参数传给构造器,执行;
如果构造器返回的是对象,则返回,否则返回第一步创建的对象。
new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。

下面代码展示了用构造器模拟类的两种方法:

function c1(){
    this.p1 = 1;
    this.p2 = function(){
        console.log(this.p1);
    }
} 
var o1 = new c1;
o1.p2();

function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
    console.log(this.p1);
}

var o2 = new c2;
o2.p2();

第一种方法是直接在构造器中修改this,给this添加属性。

第二种方法是修改构造器的prototype属性指向的对象,它是从这个构造器构造出来的所有对象的原型。

比起早期的原型模拟方式,使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数,这是一种更少坑的设计。

所以当我们使用类的思想来设计代码时,应该尽量使用class来声明类,而不是用旧语法,拿函数来模拟对象。

JavaScript中的对象分类

我们可以把对象分成几类。

  • 宿主对象(host Objects):由JavaScript宿主环境提供的对象,它们的行为完全由宿主环境决定。

  • 内置对象(Built-in Objects):由JavaScript语言提供的对象。

  • 固有对象(Intrinsic Objects ):由标准规定,随着JavaScript运行时创建而自动创建的对象实例。

  • 原生对象(Native Objects):可以由用户通过Array、RegExp等内置构造器或者特殊语法创建的对象。

  • 普通对象(Ordinary Objects):由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承。

普通对象之外的对象类型

宿主对象

JavaScript宿主对象千奇百怪,但是前端最熟悉的无疑是浏览器环境中的宿主了。

在浏览器环境中,我们都知道全局对象是window,window上又有很多属性,如document。

实际上,这个全局对象window上的属性,一部分来自JavaScript语言,一部分来自浏览器环境。

JavaScript标准中规定了全局对象属性,w3c的各种标准中规定了Window对象的其它属性。

宿主对象也分为固有的和用户可创建的两种,比如document.createElement就可以创建一些dom对象。

宿主也会提供一些构造器,比如我们可以使用new Image来创建img元素,这些我们会在浏览器的API部分详细讲解。

内置对象·固有对象

固有对象是由标准规定,随着JavaScript运行时创建而自动创建的对象实例。

固有对象在任何JS代码执行前就已经被创建出来了,它们通常扮演者类似基础库的角色。我们前面提到的“类”其实就是固有对象的一种。

ECMA标准为我们提供了一份固有对象表,里面含有150+个固有对象。

内置对象·原生对象

我们把JavaScript中,能够通过语言本身的构造器创建的对象称作原生对象。在JavaScript标准中,提供了30多个构造器。按照我的理解,按照不同应用场景,我把原生对象分成了以下几个种类。

通过这些构造器,我们可以用new运算创建新的对象,所以我们把这些对象称作原生对象。
几乎所有这些构造器的能力都是无法用纯JavaScript代码实现的,它们也无法用class/extend语法来继承。

这些构造器创建的对象多数使用了私有字段,例如:

Error: [[ErrorData]]
Boolean: [[BooleanData]]
Number: [[NumberData]]
Date: [[DateValue]]
RegExp: [[RegExpMatcher]]
Symbol: [[SymbolData]]
Map: [[MapData]]
这些字段使得原型继承方法无法正常工作,所以,我们可以认为,所有这些原生对象都是为了特定能力或者性能,而设计出来的“特权对象”。

原文摘自 winter

posted @ 2019-06-12 10:34  lisashare  阅读(253)  评论(0编辑  收藏  举报