day5(12.20) 面向对象的程序设计
理解对象的属性
理解并创建对象
理解继承
对象的属性
ECMAScript中的对象:一组名值对,其中值可以是数据或者函数。属性没有特定的顺序。
创建对象的方式:
一:通过var obj = new Object(),然后给对象属性和方法。
二:通过对象字面量var obj = {name:"王海洋”,sayName:function(){} }
数据属性:
关于数据的属性,有四个
[[configurable]],能否删除属性从而重新定义属性,能否修改属性的特性,默认true。
[[Enumerable]]能否通过for-in循环返回属性。默认true
[[Writable]]能否修改属性的值默认true
[[Value]]包含这个顺序ing的数据值,读写属性值都是这个位置。
有一个方法Object.defineProperty() 接受的参数分别是:对象、属性、数据属性的描述符对象,如下:
Object.defineProperty(obj, "name” ,{writable:false,value: “你好”,configurable:false} ) 这时候,obj的name属性,只能读不能写,不能删除,值为你好。另外注意,如果configurable改成false之后,就不能再改回true了。
访问器属性:
关于如何访问的。同样有四个:
[[configurable]],能否删除属性从而重新定义属性,能否修改属性的特性,默认true。
[[Enumerable]]能否通过for-in循环返回属性。默认true
[[Get]]读取属性调用的函数,默认undefined
[[Set]]设置属性调用的函数,默认undefined
同样也可以Object.defineProperty()定义访问器属性,比如一本书有year为2004、edition为1属性,通过Object.defineProperty(book,“year”,{get: set:})设置get和set属性,让get返回year,但是修改set,设置year为2005时候,edition为2;这是使用访问器属性的常用方式,设置一个属性,会导致其他属性发生变化。只指定getter属性意味只能读不能写。同理只指定setter只能写不能读。旧的方法中还有__defineGetter__和__defineSetter__
通过描述符一次定义多个属性,Object.defineProperties(book,{year:{value:2004} ,edition:{value:1},get:{},set:{}})
读取属性的特性:Object.getOwnPropertyDescriptor()方法。参数是对象和属性名,返回一个对象,这个对象时访问器属性或者数据属性。如:var descriptor=Object.getOwnPropertyDescriptor(book,“year”),返回一个关于year数据属性的对象,是数据属性的一个描述符。可以访问 descriptor的value或者configurable等等数据属性。同理可以返回访问器属性的描述符。
创建对象
觉得慕课网上的课程这一节讲的比较简单容易理解,贴出来:
第一类:字面量,new声明一个对象。当然也可以归于下面一类,构造函数来做。字面量时候,默认原型链指向object。
第二类:显示构造函数
第三类:第三种大多人不熟悉。
// 第一种方式:字面量
var o1 = {name: 'o1'};
var o2 = new Object({name: 'o2'});//这个不是字面量。
// 第二种方式:构造函数
var M = function (name) { this.name = name; };
var o3 = new M('o3');
// 第三种方式:Object.create
var p = {name: 'p'};
var o4 = Object.create(p);
这时候看到o4是空的,但是有name属性。
M.prototype.say = function () {
console.log('say hi');
};
var o5 = new M('o5');
var new2 = function (func) {
var o = Object.create(func.prototype);
var k = func.call(o);
if (typeof k === 'object') {
return k;
} else {
return o;
}
};
o1,o11,o2,o3这样的对象都是实例。new连接的函数,都是构造函数。
构造函数,使用new运算符,生成实例。构造函数也是函数,反过来讲,任何一个函数只要new一下,就成了构造函数,不用new就是普通函数。构造函数,也是函数。
函数都有一个prototype 属性,这是声明函数时,自动加上的,也就是原型对象。
原型对象中有一个constructor,构造器,会默认声明你构造的那个函数。
这个是我之前记忆的一个图
演示一下工作原理:
这里o3是实例,M是构造函数。
Object.create() 是E5的一个新特性哦,其实可以理解为继承一个对象,create方法有两个参数
第一个参数是要继承的原型,如果不是一个子函数,可以传一个null,第二个参数是对象的属性描述符,这个参数是可选的。
一个是要继承的对象的原型,如果没有就传null,第二个参数是对象的属性描述符,这些都是E5才有的~
vara =newObject();// 创建一个对象,没有父类哦
varb = Object.create(a.prototype);// b 继承了a的原型
这里o3是一个对象, M是构造函数,M.prototype是一个空对象,空对象里面有一个属性,constructor,构造器,指向一个函数。现在判断一下,这个constructor:function(name),这里的constructor指向了一个函数,现在判断这个函数是不是严格等于构造函数M
看一下实例o3和函数M的原型对象关系:
原型链:从一个实例对象往上找构造这个实例的相关联的对象。
简书上摘抄的某段总结:
函数的原型对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针指向上一层的原型对象,而上一层的原型对象的结构依然类似,这样利用__proto__一直指向Object的原型对象上,而Object的原型对象用Object.prototype.__proto__ = null表示原型链的最顶端,如此变形成了javascript的原型链继承,同时也解释了为什么所有的javascript对象都具有Object的基本方法。
原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。
26行之后,还要理解。意思不是很好懂
通过原型链的方式,找到原型对象, 方法被实例共享。
实例上没有找到方法,就会通过_proto_往上找,找到原型对象,原型对象上没有这个方法,那么继续像前面一样往上找。以此类推,一直到找到该方法或属性。如果一直没找到,返回,没找到。否则就返回相应的方法。
对象没有prototype的,
只有实例对象有_proto_,但是函数也会有_proto_,因为函数也是对象啊。但是它的这个属性是
上面的M是构造函数,同时也是函数,也是一个对象。也就是说它也是Function的实例,因此它的_proto_指向Function.prototype。
实例对象,instanceof引用的是构造函数的prototype这个对象。所以实例对象的这个_proto_引用的是原型对象。
其实实例对象的_proto_的指向就是最右边的原型。
另外注意一下,实例对象有一个constructor属性,指向构造函数,同时原型也有一个constructor属性指向构造函数。所以可以有:instance.constructor == instance.__proto__.constructor
实例对象 instanceof 构造函数 的时候,实际上判断的是实例对象的_proto_和构造函数的Prototype是不是引用的同一个地址。如果是,返回true。
有一点,原型有自己的构造函数。所以实例对象也是原型对象的构造函数的实例。instanceof都会返回true。如下:
o3._proto_ === M.prototype
M.prototype._proto_ === object.prototype
这里object也会被看做o3的一个构造函数。虽然不是真正的构造函数。
比如A继承了B,B继承了C,那么A的一个实例a,用instanceof判断,B,C都是true,那么怎么判断a是谁的实例?这里就要用constructor属性了。
o3的_proto_指向的是M.prototype,M.prototype的constructor指向的是函数M。所以用constructor判断比instanceof更加严谨。
听到这里,终于懵逼了。不过上面基本看的懂,还好。
1、上面过程,27行先生成一个新对象o,要指定构造函数原型对象,
2、执行构造函数,call转移上下文为o对象。
3、判断这个构造函数执行完了的结果,是不是对象类型,如果是,就是对应上面第三句话,构造函数返回了一个“对象”,那么这个对象将要会替代1中新对象o,也就是返回k,如果不是对象,那么直接返回对象o就可以了。
typeof可以判断对象和数组。不能判断Function对象。
执行上面代码,
上面验证都是正确的,说明工作原理是相同的。
也就是说new背后原理就是这样的。
下面真懵逼:
回到一个老问题,object.create和其他几个创建的对象不一样,
o4是直接拿不到name属性的。
object.create使用原型链来链接的。
也就是说o4的_proto_,指向的是p对象。
关于对象的继承(掌握五种继承方法以及各种方法的优缺点)
这块考察主要两种方式,一是直接问,面向对象的相关知识点,比如类与实例,类与继承。
还要一种问法,一个对象,继承了某个类,问你它的原型链。这块已经是上一节课讲述的内容。在一个对象上找不到一个属性,会沿着原型链往上找。
下面讲类的继承:
实现继承的基本原理,就是前面讲的原型链,继承本质就是原型链。js继承有几种形式,每种形式有哪些优缺点。如果问道面向对象,继承必问,继承方式必问。有什么不同点。其实考原型链的掌握程度。
先写一个父类Parent1,有个名字属性。写一个子类,子类的构造函数Child1 ,子类也增加一个属性。增加属性之前,通过构造函数来实现继承,既然实现继承,要体现parent1哦,所以在子类的构造函数里面执行父类的构造函数。就是Parent1.call(this),这样就实现了一个继承。call和apply都可以用,改变了函数运行的上下文。通过这种调用,在子函数中执行,同时修改了this的指向,也就是指向了Child1类的实例化的对象的引用。从而导致了父类的执行时,这个this属性,挂载到child这个类的实例对象上去。
新生成的对象,有了父级元素的name属性,实现了继承。写出来后,问你为什么能实现继承,主要原理就在40行。将父级构造函数的this指向子构造函数的实例上去。导致父级构造函数的所有属性,子类中也有。
借助构造函数实现继承缺点是什么?
Parent1也有自己的原型链和prototype,如果说Parent1除了构造函数里面的内容(这里就是this.name),还有来自原型链上的东西。这样的继承是为了改变this的指向,但是parent1原型链上的东西,并没有被child1所继承。
如果打印出say方法,会报错,就是因为没有继承父类原型对象上的方法。所以这种方式可以实现继承,但是只是部分继承,如果父类的属性都在构造函数里面,那么没有问题, 但是如果父类原型对象还有方法的话,子类是继承不到的。
第二个方法就是弥补构造函数的不足,借助原型链实现构成。
重点在55行。Child2.prototype 这里的prototype是子构造函数的属性。这个属性,是一个对象,这个对象可以任意赋值的。现在赋值给父类parent2的一个实例。new Parent2()是一个对象,这个对象给了Child2的prototype属性。57行,实例化了Child2,生成一个新的对象。新的对象有个_proto_属性,等于Child2的prototype对象。Child2的prototype对象被赋值为Parent2父类的一个实例。按照作用域的寻找方式,先找new Child2() 也就是s1 上面的属性,比如说找name,通过Child2实例化的属性上没有name,这个作用域就开始找新的对象有个_proto_属性,也就是Child2的prototype对象,同时又是Parent2()的一个实例,父类的实例,当然就有了name属性。就找到了父类的对象,父类的对象上,已经有name了。所以就是能拿到这个name,实现了继承。
这个继承方式有什么缺点:
新建s1,s2,打印出来,1,2,3。但是如果s1添加了一个4,那么s2也改变了。可是这两个实例应该隔离的,造成原因是因为原型链中的原型对象是共用的。
因为s1._proto ===s2._proto_ 这是这种继承方式的缺点。
详细内容可以参考《JavaScript高级程序设计》164-167页。
前面一个是构造函数,一个是原型链继承。组合方式就是结合这两种。结合他们的优点,弥补他们的不足。所以要在构造函数中,执行父级构造函数(70行)。再加一个自己的类型type(71行)。这里继承了构造函数那种方式的优点。下面写一下基于原型链的方法(73行)。实例化对象s3,s4,s3中加入4,然后打印。
实现了继承,结合了两种方法优点,弥补不足。写面向对象最常用方式。
但是也有缺点,父级构造函数执行了两次。比如新建的s3时候,父级65,73行分别执行了一次。没有必要两次。构造函数内的东西会自动执行,因此没必要再把它的内容放到原型对象上去。没必要这么写。
构造函数体能内通过两个函数的组合,能拿到所有的构造函数体内的属性和方法。现在既然先继承父类的原型对象,所以直接赋给我当前的原型对象就行了(91行)。这样就可以把父级构造体内的原型属性和方法拿到,也能把原型对象的属性和方法拿到。完全实现继承。打印5,6
两个都实现了继承。父类只执行了一次。不会再执行父级构造函数。
这种方式还有不足:s5 instanceof Child4 ; s5 instanceof Parent4 都是ture
那么怎么区分一个对象时子类直接实例化,还是父类直接实例化。
还有个方法用来判断对象是不是某一个类的实例,是通过constructor。
结果指向了parent4 ,显然不是想要的。
为什么这个效果?
prototype里面有一个属性叫constructor,由于Child4.prototype = Parent4.prototype;所以Child4.prototype和Parent4.prototype是一个对象,Child4.prototype里面的constructor就是Parent4.prototype中的constructor,而Parent4.prototype中的constructor当然指向的是parent4。产生这个问题根源就是共用了一个原型对象(91行)。所以就是会出现这个问题喽。其实上上面,s3也有这个问题。
Object.create( )创建对象的原理。110行。与91行不同,91行,两个对象构造函数是一个。Child4.prototype和Parent4.prototype是一个对象,constructor也是同一个。无法区分实例是父类还是子类创造。110行解决了这个问题。通过Object.create()方法会使用指定的原型对象及其属性去创建一个新的对象。通过Object.create()创建了一个中间对象,这个中间对象有一个特性,他的原型对象时父类的原型对象。
这样的话原型链上连起来了。可以正确区分父类和子类的构造函数了。
为什么通过Object.create()创建的对象能作为他两的桥梁?Object.create()创建的这个对象,原型对象就是参数。所以child5的原型对象是child5.prototype,这个对象又等于Object.create()创建的新对象,新兑现的原型对象时括号中的参数,也就是Parent5.prototype。连起来了。这样就形成了继承,而且实现了父类和子类原型对象的隔离。这就是原理。
这时候虽然隔离了,但是child5.prototype依然没有自己的constructor,它要找的话,依然是通过原型链往上找,找的还是parent5.prototype上去。所以要给child5的原型对象写一个自己的constructor。Child4.prototype.constructor = Child5;那为什么不在Child4.prototype = Parent4.prototype 上写一个constructor?这是不行的,因为两个的原型对象引用的是同一个,一改的话,都改了。父类子类的都改了。又没办法区分父类的。
两个建议:原型链东西,继承消化一下。
面试技巧:能答上前两个比较多,组合方式比较少。是不是直接把最优的方法写出来呢?不建议,原因很简单,时间是一定的。不要让他问你太多题目。15道题,你回答4个,和8道题你回答8个,给面试官感觉是不一样的。利用技巧,彰显自己在原型链方面掌握的足够扎实。所以让你写继承方式的时候,五种都写,然后问问要不要讲讲这几种的区别?面试官同意,就娓娓道来。不拖沓。很多人都是挤牙膏,问一种,有啥缺点,很能不能写,再说一种,非常的让面试官反感。
懵逼状态,等我把JS高级程序设计这一节再撸一遍之后再重新看。