一 实例对象与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对象。
但是,如果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命令。
4. Object.create()创建实例对象
构造函数作为模板,可以生成实例对象。但是,有时候拿不到构造函数,只能拿到一个现有的对象。我们希望能拿这个现有的对象作为模板,生成新的实例对象,这时候就可以使用 Object.create()方法了。
二 this关键字
1.涵义
this 关键字是一个非常重要的语法点,毫不夸张的讲,不理解它的含义,大部分开发任务都无法完成。
前面已经提到了,this在构造函数中,表示实例对象。除此外,this还可以用在别的场合里。不管什么场合,this都有一个共同特点,返回一个对象。
简单点说,this就是属性或者方法‘当前’的对象。
上面代码中,this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用,而describe方法所在的当前对象是person,因此this指向person,this.name就是person.name。
由于对象的属性是可以赋值给另一个对象的,所以属性所在对象是会发生改变的,即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 稍微改动一下让 第二个this也指向当前对象o:
2.避免数组处理方法中的this
数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this。
解决这个问题的一种方法,就是前面提到的,使用中间变量固定this。
或者固定运行环境的办法也可以。
3.绑定this的方法
JavaScript提供了call apply bind三个方法可以切换/固定 this指向。
1)Function.prototype.call()
格式 func.call(thisValue, arg1, arg2, ...)
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)都会生成这两个属性,即这两个属性会定义在实例对象上面。
同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
这个问题的解决方法,就是 JavaScript 的原型对象(prototype)
1.2 prototype 属性的作用
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系
JavaScript规定,每个函数都有一个prototype属性,指向一个对象。
对于普通函数来说,该属性基本无用。但是对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
Animal的prototype属性,就是实例对象cat1和cat2的原型对象。原想对象添加color属性,实例对象都共享了该属性。
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现所有实例对象上。
原型对象的color属性的值变为yellow,两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。
如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。
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的实例对象,但是f1自身没有constructor属性最后一行代码就返回了false,该属性其实是读取原型链上面的F.prototype的constructor属性 。
constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错。
上面代码中,构造函数Person的原型对象改掉了,但是没有修改constructor属性,导致这个属性不再指向Person。由于Person的新原型是一个普通对象,而普通对象的constructor属性指向Object构造函数,导致Person.prototype.constructor变成了Object。
所以,修改原型对象时,一般要同时修改constructor属性的指向。
2. instanceof运算符
instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。
instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的。
由于instanceof检查整个原型链,因此同一个实例可能会对 多个构造函数返回true。
有一种情况比较特殊,就是做左边对象的原型链上,只有null对象,这时候,instanceof就会判断失误。
上面代码中,Object.create(null)返回一个新对象obj,它的原型是null(Object.create的详细介绍见后文)。右边的构造函数Object的prototype属性,不在左边的原型链上,因此instanceof就认为obj不是Object的实例。但是,只要一个对象的原型不是null,instanceof运算符的判断就不会失真。
instanceof运算符还可以判断值的类型。
注意:instanceof只能用于对象,不适用原始类型的值(String 布尔值 数值 三个原始类型 不能再细分了 ,对象是一个合成型值)。
3. 构造函数的继承
让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。第一步再子类的构造函数中,调用父类构造函数。第二步让子类的原型指向父亲的原型,这样子类就能继承父亲的原型。
4.多重继承
JavaScript不提供多重继承功能,即不允许一个对象继承多个对象。但是,通过变通的办法,实现。
5.模块
随着网站逐渐变成“互联网应用程序”,潜入网页的Js代码越来越大,越来越复杂。网页越来越像桌面程序,需要一个团队分割写作,进度管理,单元测试等等...开发者必须使用软件工程的方法,管理网页的业务逻辑。
JavaScript模块化编程,已经编程一个迫切需求。理想情况下,开发正只需要实现核心业务逻辑,其他的都可以加载被人已经写好的模块。
但是,JavaScript并不是一种模块化编程语言,ES6才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块效果。
5.1模块基本的实现方法
模块是实现特定功能的一组属性和方法的封装。
简单的做法就是把模块写成一个对象,所有模块的成员都放到这个对象里。
但是!这样的写法会暴漏所有模块成员,内部状态可以被外部改写。比如外部代码可以直接改变内部计数器的值。
这怎么办呢?我们可以利用构造函数,封装私有变量。
5.2 封装私有变量:构造函数写法
上面的代码,buffer是模块的私有变量。一单生成实例对象,外部是无法访问buffer的。但是,这种方法将私有变量封装在构造函数中,倒是构造函数与实例对象是一体的,总是存在内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又保存实例对象的数据,违背了构造函数与实例对象在数据相分离的原则(即实例对象的数据不应该保存在实例对象以外)同时又非常消耗内存。
有没有更好的办法???慢慢往下看👇
5.3 封装私有变量:立即执行函数的写法
另一种做法就是使用‘立即执行函数’,将相关的属性和方法封装在一个函数作用域里面,可以达到不暴漏私有成员目的。
上面的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()方法返回参数对象的原型。这是获取原型对象的标准方法。
下面是几种特殊的对象原型:
2.Object.setPrototypeOf()
Object.setPrototypeOf() 方法为参数对象设置原型对象,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。
new命令可以使用Object.setPrototypeOf()方法模拟:
上面代码,new命令新建实例对象,其实可以分成两步。第一步,将一个空对象的原型设为构造函数的prototype属性(上面的例子 就是 F构造函数的prototype属性, F.prototype原型对象);第二部将构造函数内部的this绑定这个空对象,然后执行构造函数,使得定义在this上面的方法和属性(上例就是this.name),转移到这个空对象上。
3.Object.create()
生成实例对象的常用方法是,使用new命令让构造函数 返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?
JavaScript提供了Object.create方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例对象完全继承原型对象的属性。