博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JavaScript面向对象详解(原理)
阅读量:5904 次
发布时间:2019-06-19

本文共 6525 字,大约阅读时间需要 21 分钟。

概述

JavaScript中的面向对象是基于原型链来实现的,这不同于其他语言复制拷贝的方式。我觉得原型链的好处是节约内存,提高性能,缺点可能就是不那么容易理解。

下面我们就来循序渐进的通过原型链,来理解JavaScript中的面向对象。

面向对象的概念是为了解决什么问题?

如果我们想创建一个具有一定功能的集合,在JavaScript中我们可以这样写:

var Animal = {    name: 'kitty',    sleep: function(){        console.log(this.name + " is sleeping");    }};复制代码

即通过对象的形式,将一些属性(如name)或方法(如sleep)包装在一起,这样就形成了逻辑上的集合。

但是如果我们想再创建一个类似的对象呢?最直接的方式是这样:

var Animal = {    name: 'kate',    sleep: function(){        console.log(this.name + " is sleeping");    }};复制代码

也就是重新再写一个对象,修改其中的部分属性。这种方式无疑是非常僵硬的,因此可以通过这种“工厂函数“的方法:

function createAnimal(name){    var obj = {}    obj.name = name;    obj.sleep = function(){        console.log(this.name + " is sleeping");    }    return obj;}复制代码

可以看到,”工厂函数“的作用,即在内部创建一个空的对象,然后将属性和方法填充进去,最后再返回这个对象。

这样看起来似乎没什么问题,但是仔细想想,其实我们只不过是想按照一定的模式创建一个对象,逻辑上来讲,我们要做的应该只是提供模式,而新建一个空对象,返回一个空对象的操作其实没有必要由我们来完成,因此JavaScript就提出了一个new关键字。这个关键字的用法很简单:

function Animal(name){    this.name = name;    this.sleep = function(){        console.log(this.name + " is sleeping");    }}var kitty = new Animal("kitty");//如果我们在浏览器控制台打印kitty,得到的是这样的一个对象:{ name: "kitty" sleep: function sleep()}复制代码

即在调用Animal函数前,加上一个new关键字,告诉编译器,这个函数是一个”构造函数“,我们应该调用这个函数,并且根据函数所指定的规则,包装一个对象,并返回这个对象。

tips:后面在了解的原型链后我们会自己实现一个new

JavaScript中的原型链

前面我们已经知道了为什么需要面向对象,下面我们就来看看JavaScript语言处理面向对象的巧妙之处——原型链

先来看一张图

先来从上往下讲解一下这张图:

  • 函数function)都有一个prototype属性,指向一个原型对象
  • 原型对象都有一个constructor属性,指向这个原型对象对应的构造函数
  • 普通对象都有一个__protor__(ES并没有规定这个属性的标准名称,而浏览器大多都用__proto__访问这个属性),指向这个普通对象原型对象

我们还是拿上面的那个例子来看看:

function Animal(name){    this.name = name;    this.sleep = function(){        console.log(this.name + " is sleeping");    }}var kitty = new Animal("kitty");复制代码

我们先在浏览器中打印出kitty:

可以看到kitty即一个对象,这个对象直接包含name属性和sleep方法。可以看到还有一个灰色的属性:<prototype>对象,这就是前面我们提到的__proto__,(完全是同一个东西,只不过火狐浏览器这样显示罢了,为了保持统一,我们在本文就叫它__proto__)。

我们展开这个__proto__看看它的组成:

很简单嘛,也就是一个包含constructor属性的对象,这个constructor属性指向kitty的构造函数Animal。

注意,在这里,__proto__所指向的对象也有一个<prototype>,这是理所当然的,因为这个属性所有的对象都有(甚至函数也有,因为函数其实也是一种对象)

我们再在浏览器中打印出Animal这个函数:

可以看到这个函数有一个prototype属性,我们已经可以看到这个prototype属性是一个对象(即原型对象)。我们再展开这个prototype属性看看:

可以看到目前这个原型对象非常简单,就是一个constructor,指向了Animal这个函数。

那么如果我们想原型对象中添加一些东西呢?像这样:

Animal.prototype.eat = function (food){    console.log("eat " + food);}复制代码

加上这段代码,我们刷新一下页面,再看看这时的原型对象是什么:

果然,eat 方法被添加到了Animal的原型对象中。我们在kitty中调用这个方法试试看:

kitty.eat("shxt");//打印结果:eat shxt复制代码

好,到此为止,我们已经大致了解了原型链的存在形式:即通过原型对象来链接,下面我们总结一下:

  • 创建一个函数的时候,会创建这个函数对应的原型对象,原型对象的constructor指向这个函数。
  • 函数的prototype属性,以及函数(通过new)创建的实例对象的__proto__属性,都指向同一个原型对象。

而在原型链的作用在于:

比如上面我们在Animal的原型对象上定义了一个eat方法。我们有一个Animal的实例kitty,我们打印出kitty:

发现kitty对象中并没有eat方法,那么它是怎么调用到eat的呢?没错,我们可以看到kitty对象的原型对象中有eat这个方法。所以在原型链机制中,对象的方法和属性的调用过程是这样的:

  1. 先在自己当前对象寻找这个属性或方法,如果找到了就直接用
  2. 如果找不到,就去当前对象的__proto__属性指向的原型对象中寻找,如果找到了,就使用
  3. 如果还找不到,就在当前原型对象的__proto__属性指向的上一级原型对象寻找
  4. 如果没有更上一级的原型对象,那么__proto__属性会指向Object的原型对象(这个对象就没有__proto__属性了)
  5. 如果在Object的原型对象中还找不到,那么就返回undefined

Object的原型对象长这样:

是不是看着很熟悉,里面有很多我们常用的方法。

那就顺便再把function的原型对象放出来:

是不是更熟悉了?而且可以看到function的原型对象的__proto__属性,指向Object的原型对象。

所谓原型链,就是一个对象的__proto__指向一个原型对象,而这个原型对象的__proto__又指向另一个原型对象,依次串联下去。

目前我们谈论的原型链都是很短的,要想凸显原型链的威力,就要讲讲继承了。

JavaScript中的继承

所谓继承就是,在一个父类的基础上,创建一个子类,子类拥有父类的属性和方法,也有自己的属性和方法。

在JavaScript中,实现继承主要思想是:

拿到子类的构造函数的原型对象,将其__proto__属性指向父类构造函数的原型对象。

这样子类构造函数生成的实例对象就可以先访问到子类的原型对象,再访问到父类的原型对象,就完成了继承。

具体的实现方式呢,有多种:

一、实现子类构造函数,在子类构造函数上修改原型链

这种是最传统的做法,步骤是:

  • 创建一个子类构造函数
  • 在这个构造函数内调用父类的构造函数
  • 将子类构造函数的原型对象的__proto__属性指向父类构造函数的原型对象

这样,当子类创建的实例中寻找属性或方法时,先找到子类的原型对象,找不到的话就去子类原型对象的__proto__(即父类的原型对象)找,这样就实现了继承。

我们来实现一个例子:

function Animal(name) {    this.name = name;    this.sleep = function () {        console.log(this.name + " is sleeping");    }}Animal.prototype.eat = function (food) {    console.log(this.name + " is eating " + food);}function Cat(name, color) {    Animal.call(this, name); //获得Animal内部定义的属性和方法    this.color = color;}//Object.create()方法作用是,创建一个空的对象,将这个对象的__proto__属性指向参数Cat.prototype = Object.create(Animal.prototype); //获得Animal原型对象上的方法和属性//这里暂时先不考虑原型对象的constructor属性的指向是否正确//我们打印出来看看Cat构造函数console.log(Cat);//下面我们可以新建一个Cat实例var kitty = new Cat("kitty","yellow");console.log(kitty);//试试使用父类的原型上的方法kitty.eat("fish"); //console: kitty is eating fish复制代码

你可以复制我的代码到浏览器的控制台,看看打印出来的结果是怎样的。

但是这种方法有一个缺点,即对于某些JavaScript内建对象(如Date),如果实例对象不是由它本身的构造函数生成的,不能访问其内部的属性和方法。所以通过这种方法继承Date类,即便我们修改了原型链,但还是不能调用Date内的方法。不信?我们来验证一下:

function MyDate(date) {    Date.call(this, date);    this.log = function () {        console.log("now is " + date);    }}MyDate.prototype = Object.create(Date.prototype);var time = new MyDate("2018-08-23");time.getDate(); //console: TypeError: getDate method called on incompatible Object复制代码

虽然我们已经将MyDate的原型对象的属性指向Date的原型对象了,但是还是不能调用Date中的方法。

二、创建父类实例,在父类实例上修改原型链

解决上面的问题其实也很简单,即我们先用Date构造函数生成一个实例对象,然后将这个实例对象改造成子类实例对象,这样就不会出现上面这种问题。所以这种方式的步骤是这样的:

  • 用父类构造函数创建实例
  • 将实例的__proto__属性指向子类的原型对象
  • 将子类原型对象的__proto__指向父类的原型对象

这样,父类原型对象又被链接到子类上了,而且我们的实例也是通过父类创建出来的,也就不会出现上面的那种限制了。

我们来实现一下:

//先来创建子类的构造函数function MyDate() {}MyDate.prototype.log = function () {    return this.getDate();}var time = new Date(); //创建父类实例Object.setPrototypeOf(time, MyDate.prototype); //实例的__proto__指向子类原型对象Object.setPrototypeOf(MyDate.prototype, Date.prototype); //子类原型对象指向父类原型对象console.log(time.log());复制代码

这个方法其实也有缺点,根据MDN文档,Object.setPrototypeOf()方法是十分浪费性能的,所以除非迫不得已,还是少用这种方法。

拓展

关于JavaScript中的面向对象我们已经讲完了,下面就来尝试实现几个原生的API:new,Object.create()

new

new是一个关键字,我们自然不能创建一个关键字,我们这里就创建一个new函数:

function MyNew() {    //注意,这个实现没有进行错误处理,仅考虑了参数合理的情况,为了理解new已经够了        var obj = {};    //获取构造函数和参数    var Cons = [].shift.call(arguments); //这个操作有的人可能看的有点晕,    								  //这么写是因为arguments其实不是一个数组    								  //它仅有一个length属性,所以没有数组的方法。    //将实例对象的__proto__属性指向构造函数的prototype属性    obj.__proto__ = Cons.prototype;    //调用构造函数    Cons.apply(obj, arguments);    return obj;}复制代码

我们来加上一些代码,测试一下:

function Animal(name) {    this.name = name;}Animal.prototype.eat = function (food) {    console.log(this.name + " is eating " + food);}//测试一下var kitty = MyNew(Animal, "kitty");console.log(kitty.name); //console: kittykitty.eat("fish"); //console: kitty is eating fish复制代码

Object.create()

Object.prototype.myCreate = function (proto) {    //注意,这里也省略了错误处理,并且真实的create方法有第二个参数    function F() {}; //创建一个空的构造函数    F.prototype = proto; //将该函数的prototype指向属性proto(传入的原型对象)    return new F(); //返回的对象的__proto__根据F构造函数的prototype设置,    //因此返回一个仅有__proto__属性(指向传入的原型对象)的空对象。}//测试console.log(Object.myCreate(Date.prototype)); //在浏览器打印出来得到要一个对象										  //对象的__proto__指向Date原型对象。复制代码

参考资料:

InfoQ * Interview Map 《前端面试指南》

转载于:https://juejin.im/post/5b829fd2e51d4538dc2bf83b

你可能感兴趣的文章
时序约束优先级_Vivado工程经验与各种时序约束技巧分享
查看>>
minio 并发数_MinIO 参数解析与限制
查看>>
flash back mysql_mysqlbinlog flashback 使用最佳实践
查看>>
mysql存储引擎模式_MySQL存储引擎
查看>>
java 重写system.out_重写System.out.println(String x)方法
查看>>
配置ORACLE 11g绿色版客户端和PLSQL远程连接环境
查看>>
ASP.NET中 DataList(数据列表)的使用前台绑定
查看>>
Linux学习之CentOS(八)--Linux系统的分区概念
查看>>
System.Func<>与System.Action<>
查看>>
asp.net开源CMS推荐
查看>>
csharp skype send message in winform
查看>>
MMORPG 游戏服务器端设计--转载
查看>>
HDFS dfsclient写文件过程 源码分析
查看>>
ubuntu下安装libxml2
查看>>
nginx_lua_waf安装测试
查看>>
WinForm窗体缩放动画
查看>>
JQuery入门(2)
查看>>
linux文件描述符
查看>>
传值引用和调用引用的区别
查看>>
hyper-v 无线网连接
查看>>