美文网首页
面向对象编程(基础篇)

面向对象编程(基础篇)

作者: 梵仇不是大侠 | 来源:发表于2019-04-17 15:58 被阅读0次

    一  实例对象与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方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例对象完全继承原型对象的属性。

    相关文章

      网友评论

          本文标题:面向对象编程(基础篇)

          本文链接:https://www.haomeiwen.com/subject/rbgbwqtx.html