面向对象编程(基础篇)

一  实例对象与new命令


1. 什么是对象?

面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

那么,“对象”(object)到底是什么?我们从两个层次来理解

    1.1 对象是单个实物的抽象

        一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

    1.2 对象是一个容器,封装了属性(property)和方法(method)。

        属性是对象的状态,方法是对象的行为(完成某种任务)。举个例子:我们可以把动物抽象为 animal对象,使用“属性”来记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑,捕猎,交配,休息...等等)


2. 构造函数

面向编程的第一步,就是要生成对象,前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。典型的面向对象变成语言(java 和c++),都有‘类’(class)的概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是Javascript不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

Javascript语言使用构造函数(constructor)作为对象的模板。所谓的“构造函数",就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数可以生成多个实例对象,这些实例对象都具有相同的结构。

构造函数(constructor)就是一个很普通的函数,但是有自己的特征和用法。


上面的代码 Person就是一个构造函数,为了与普通函数区别,构造函数的名字的第一个字母通常大写。

构造函数(constructor)的两个特点:

                函数体内使用了 this关键字,代表了所要生成的对象实例。

                生成对象时候,必须使用 new 命令。


3. new命令

3.1 基本用法

new命令的作用,就是执行构造函数,返回一个实例对象。


上面代码通过new命令,让构造函数Person生成一个实例对象,保存在变量person1中。这个新生成的实例对象,从构造函数Person得到了name属性。new命令执行时,构造函数内部的this,就代表了新生成的实例对象,this.name表示实例对象有一个name属性,值是 Yahiko。

使用new命令时候,构造函数也是可以接受参数的。


3.2 new命令的原理

使用new命令时,它后面的函数依次执行下面的步骤:

1)创建了一个空对象,作为要返回的实例对象。

2)将这个空对象的原型,指向构造函数的 prototype属性。

3)将这个空对象赋值给函数内部的this关键字。

4)开始执行函数内部代码。

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。


构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。

但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。


上面代码中,构造函数Vehicle的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。

另一方面,如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象。


上面代码中,getMessage是一个普通函数,返回一个字符串。对它使用new命令,会得到一个空对象。这是因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象。本例中,return语句返回的是字符串,所以new命令就忽略了该语句

3.3  new.target

函数内部可以使用new.target属性。如果函数是new命令调用的,new.target指向当前函数,否则为undefined。


new.target这个属性可以判断函数调用时,是否使用了new命令。


上述代码没有 用new 命令  直接Fun() 调用,导致控制台直接扔出一个错误,传来一顿训斥



4. Object.create()创建实例对象

构造函数作为模板,可以生成实例对象。但是,有时候拿不到构造函数,只能拿到一个现有的对象。我们希望能拿这个现有的对象作为模板,生成新的实例对象,这时候就可以使用 Object.create()方法了。


上面代码中,对象person1是person2的模板,后者继承了前者的属性和方法。




二  this关键字

1.涵义

this 关键字是一个非常重要的语法点,毫不夸张的讲,不理解它的含义,大部分开发任务都无法完成。

前面已经提到了,this在构造函数中,表示实例对象。除此外,this还可以用在别的场合里。不管什么场合,this都有一个共同特点,返回一个对象。

简单点说,this就是属性或者方法‘当前’的对象。


上面代码中,this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用,而describe方法所在的当前对象是person,因此this指向person,this.name就是person.name。

由于对象的属性是可以赋值给另一个对象的,所以属性所在对象是会发生改变的,即this的指向是可变的。

person.describe属性被赋给person2,于是person2.describe就表示describe方法所在的当前对象是person2,所以this.name就指向person2.name

拆分一下上面的例子,重构一下:


readName函数f内部使用了this关键字,随着f所在的对象不同,this的指向也不同

总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最让初学者感到困惑的地方。

教你个笨招数,你不是很明确this指向的时候,看的晕头转向的时候,别再靠猜了!不妨console.log()打印一下这个this,你看看当前它到底指向谁。

2. this实质

javascript语言之所以有this设计,跟内存里面的数据结构有关。

var  obj={ foo:5}

上面的代码,将一个对象赋值给了变量obj。Javascipt引擎会现在内存里面,生成一个对象{foo:5},然后再把这个对象的内存地址赋值给变量obj。

也就是说,变量obj 是一个地址 。后面要读取 obj.foo,引擎先从obj拿到内存地址,再从该地址读取原始对象,返回了foo属性。

3.使用场景

1.全局环境

全局环境使用this ,它的指向就是window。

2.构造函数

构造函数中的this,指的是实例对象。

3.对象的方法

如果对象的方法里包含了this,this的指向就是该方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。(这种情况不好把握)

4.注意事项

1.this 尽量避免多层

由于this的指向不确定,所以切勿在函数中多层this,当然了 也有办法搞,你就想多层套这咋办呢?使用一个变量固定this的值,然后内层函数调用这个变量。举个例子:


第一个this指向的是对象o也就是Object,第二个this指向的就是顶层对象window

这时候我们可以在第二个this 稍微改动一下让 第二个this也指向当前对象o:


这就是使用一个变量固定this的值,然后内层函数调用这个变量  简单点说 重新把this赋值了给了that变量 that指向当前对象

2.避免数组处理方法中的this

数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this。


foreach方法的回调函数中的this,其实是指向window对象,因此取不到o.v的值。

解决这个问题的一种方法,就是前面提到的,使用中间变量固定this。


中间变量固定this 赋值给that

或者固定运行环境的办法也可以。


固定运行环境 给forEach加个第二参数

3.绑定this的方法

JavaScript提供了call apply bind三个方法可以切换/固定 this指向。

1)Function.prototype.call()

  格式 func.call(thisValue, arg1, arg2, ...)


全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象o

call方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象,也可以传入第多个参数,第一个参数是this指向的对象,后面的参数则是函数调用时所需参数。


2)Function.prototype.apply()

格式 func.apply(thisValue, [arg1, arg2, ...])

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。



三 对象的继承

面向对象编程很重要的一个方面,就是对象的继承。A对象通过继承B对象,就能直接拥有B对象的所有属性和方法,这对代码复用很有用。

大部分面向对象语言都是通过“类”实现对象继承。传统上,Javascript语言不通过class,而是通过“原型对象”(prototype)实现。

es6引入了class语法 ,先暂时不说。后续再专门写关于ES6部分的。

1 原型对象概述

    1.1构造函数的缺点

JavaScript通过构造函数申城新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。


上面代码中,Cat函数是一个构造函数,函数内部定义了name属性和color属性,所有实例对象(上例是cat1)都会生成这两个属性,即这两个属性会定义在实例对象上面。

同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。


cat1和cat2都是同一个构造函数的实例,生成了两个meow方法,浪费资源

这个问题的解决方法,就是 JavaScript 的原型对象(prototype)

    1.2 prototype 属性的作用

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系

JavaScript规定,每个函数都有一个prototype属性,指向一个对象。


对于普通函数来说,该属性基本无用。但是对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。


Animal的prototype属性,就是实例对象cat1和cat2的原型对象。原想对象添加color属性,实例对象都共享了该属性。

原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现所有实例对象上。

修改了原型对象上 color属性值

原型对象的color属性的值变为yellow,两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。


cat1和cat2实例都有color这个属性 ,就不会再去找原型对象上的colo属性了

总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。

    1.3 原型链

JavaScript规定,所有对象都有自己的原想对象(prototype)。一方面,任何一个对象,的都可以充当其他对象的原型,另一方面,由于原型对象也是对象,所以他特有自己的原型。因此,就会形成一个“原型链”:对象到原型对象,原型对象到原型对象的原型对象...

如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOf和toString方法的原因,因为这是从Object.prototype继承的。

那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。

注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

var A=function(){};

var a=new A();

A是构造函数,a是构造函数A的实例。A.prototype可以看作一个整体 他就是原型对象。

挂在A.prototype上的属性或方法,都可以被实例a调用。

实例a.__proto__=(构造函数A.prototype)原型对象    

下面图便于理解:


    1.4 constructor 属性

prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。


由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承。


实例f1继承了 F.prototype原型对象上的constructor属性

上面代码中,f1是构造函数F的实例对象,但是f1自身没有constructor属性最后一行代码就返回了false,该属性其实是读取原型链上面的F.prototype的constructor属性 。

constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错。


上面代码中,构造函数Person的原型对象改掉了,但是没有修改constructor属性,导致这个属性不再指向Person。由于Person的新原型是一个普通对象,而普通对象的constructor属性指向Object构造函数,导致Person.prototype.constructor变成了Object。

所以,修改原型对象时,一般要同时修改constructor属性的指向。


要么将constructor属性重新指向原来的构造函数,要么只在原型对象上添加方法,这样可以保证instanceof运算符不会失真

    2. instanceof运算符

instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。


instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的。


由于instanceof检查整个原型链,因此同一个实例可能会对 多个构造函数返回true。


f同时时F和Object的实例,两个构造函数都返回true

有一种情况比较特殊,就是做左边对象的原型链上,只有null对象,这时候,instanceof就会判断失误。


上面代码中,Object.create(null)返回一个新对象obj,它的原型是null(Object.create的详细介绍见后文)。右边的构造函数Object的prototype属性,不在左边的原型链上,因此instanceof就认为obj不是Object的实例。但是,只要一个对象的原型不是null,instanceof运算符的判断就不会失真。

instanceof运算符还可以判断值的类型。


注意:instanceof只能用于对象,不适用原始类型的值(String 布尔值 数值 三个原始类型 不能再细分了 ,对象是一个合成型值)。


String是原始类型值 instanceof并不适用

    3. 构造函数的继承

让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。第一步再子类的构造函数中,调用父类构造函数。第二步让子类的原型指向父亲的原型,这样子类就能继承父亲的原型。


Son构造函数继承了Father构造函数


    4.多重继承

JavaScript不提供多重继承功能,即不允许一个对象继承多个对象。但是,通过变通的办法,实现。


子类Son同时继承了父类M1和M2。这种模式又称为 Mixin(混入)。

    5.模块

随着网站逐渐变成“互联网应用程序”,潜入网页的Js代码越来越大,越来越复杂。网页越来越像桌面程序,需要一个团队分割写作,进度管理,单元测试等等...开发者必须使用软件工程的方法,管理网页的业务逻辑。

JavaScript模块化编程,已经编程一个迫切需求。理想情况下,开发正只需要实现核心业务逻辑,其他的都可以加载被人已经写好的模块。

但是,JavaScript并不是一种模块化编程语言,ES6才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块效果。

        5.1模块基本的实现方法

模块是实现特定功能的一组属性和方法的封装。

简单的做法就是把模块写成一个对象,所有模块的成员都放到这个对象里。


但是!这样的写法会暴漏所有模块成员,内部状态可以被外部改写。比如外部代码可以直接改变内部计数器的值。


这怎么办呢?我们可以利用构造函数,封装私有变量。

        5.2 封装私有变量:构造函数写法


上面的代码,buffer是模块的私有变量。一单生成实例对象,外部是无法访问buffer的。但是,这种方法将私有变量封装在构造函数中,倒是构造函数与实例对象是一体的,总是存在内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又保存实例对象的数据,违背了构造函数与实例对象在数据相分离的原则(即实例对象的数据不应该保存在实例对象以外)同时又非常消耗内存。


这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。

有没有更好的办法???慢慢往下看👇

        5.3 封装私有变量:立即执行函数的写法

另一种做法就是使用‘立即执行函数’,将相关的属性和方法封装在一个函数作用域里面,可以达到不暴漏私有成员目的。


重新给count赋值,并不能改变成员

上面的module就是 JavaScript 模块的基本写法。下面,再对这种写法进行加工。再来👇

        5.4模块的放大模式

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用‘放大模式’。


上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。

在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"(Loose augmentation)。


与"放大模式"相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象。

        5.5输入全局变量

独立性是模块的重要特点,模块内部最好不要与程序进行直接交互。

为了在模块内调用全局变量,必须显式的将其他变量输入模块。


上面的moudle模块需要使用jQuery库和YUI库,就把这两个库(起始式两个模块)当作参数输入moudle。这样做除了保证模块独立性,还使得模块之间的依赖关系变得明显。

立即执行函数还可以起到命名空间的作用:


上面代码中,finalCarousel对象输出到全局,对外暴露init和destroy接口,内部方法go、handleEvents、initialize、dieCarouselDie都是外部无法调用的。



四 Object对象的相关方法

1.Object.getPrototypeOf()

Object.getPrototypeOf()方法返回参数对象的原型。这是获取原型对象的标准方法。


上面代码中,实例对象f的原型式F.prototype

下面是几种特殊的对象原型:


2.Object.setPrototypeOf()

Object.setPrototypeOf() 方法为参数对象设置原型对象,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。


将a对象的原型对象设置为b, a对象就共享了b对象的属性x

new命令可以使用Object.setPrototypeOf()方法模拟:


上面代码,new命令新建实例对象,其实可以分成两步。第一步,将一个空对象的原型设为构造函数的prototype属性(上面的例子  就是 F构造函数的prototype属性,   F.prototype原型对象);第二部将构造函数内部的this绑定这个空对象,然后执行构造函数,使得定义在this上面的方法和属性(上例就是this.name),转移到这个空对象上。

3.Object.create()

生成实例对象的常用方法是,使用new命令让构造函数 返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?

JavaScript提供了Object.create方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例对象完全继承原型对象的属性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容