从去年开始阅读红宝书,从第一遍看不懂,到后来慢慢理解,这个过程还是很痛苦的(已经粗略翻看了6遍了o(╥﹏╥)o),所以突然打算记录下看书学习过程中总结的笔记,并抽这个空再一次回顾下基础知识,对于我来说,每看一遍感觉都是不一样的,都会有很大的收货(前端路漫漫o(╥﹏╥)o)
p为Person的实例化对象 / var p =new Person()
方法 | 作用 |
---|---|
instanceof | 检测对象是否为另一个对象的实例 p instanceof Person |
isPrototypeOf | 无法访问[[Prototype]] 可以使用isPrototyprOf()来确定是否存在这种关系 Person.prototype.isPrototypeOf(p) |
Object.getPrototypeOf() | 可以获取一个实例对象的原型 Object.prototypeOf(p)==Person.prototype |
hasOwnProperty | p.hasOwnProperty('name') true来自实例 false说明来自原型 |
Object.getOwnPropertyDescriptor() | 只能用于实例属性获取,要想取得原型属性的描述符,必须直接在原型对象上调用此方法 |
Object.keys() | 接受一个对象的参数,返回一个包含所有可枚举属性的字符串数组 |
Object.getOwnPropertyNames() | 返回的所有实例属性,枚举和不可枚举都有 Object.getOwnPropertyNames(Person.prototype) |
Object.create() | 参数是一个作为新对象原型的对象和一个为新对象定义额外属性的对象 |
更多 | 更多 |
1. ECMA两种属性类型
数据属性
[[configurable]] 默认true
1.表示能都否通过delete删除属性而重新定义属性,
2.能否修改属性的特性,
3.能否把属性修改为数据属性
[[enumerable]]
表示能否使用for-in循环返回属性 默认true
[[writable]]:
表示能否修改属性的值
[[value]]
包含该属性的数据值。默认为undefined
vue.js中就是采用此方法,所以不支持IE8
当对象里面有属性,采用Object.defineProperty方法只是修改特性和值时,默认值都为true,
访问器属性
[[configurable]] 默认true
1.表示能都否通过delete删除属性而重新定义属性,
2.能否修改属性的特性,
3.能否把属性修改为数据属性
[[enumerable]]
表示能否使用for-in循环返回属性 默认true
[[get]]
表示读取属性时调用 默认 undefined
[set]
表示写入属性时调用 默认undefined
案例
/*
数据属性:定义了一个对象Person 其中name的属性指定的值就是[[value]]的值 ,这个值的任何修改状况都将反映在value数据属性的特性上
*/
var Person={
name:"梵高先生"
};
/*
要修改默认的特性,必须使用ES5的Object.defineProperty()方法
arguments参数为
1.属性所在的对象,
2.属性的名字,
3.一个描述符(descriptor)对象(属性必须是其中之一或多个)
Object.defineProperty(Person,'name',{
value:"达芬奇" // /提示:我们可以手动进行修改,此时Person.name="达芬奇"
})
*/
Object.defineProperty(Person,'name',{
writable:false, //表示能否修改属性值
// configurable:false 一旦把属性定义为不可配置的,就再也无法改变回来; 可多次调用方法修改同一个属 性,但是configural设置为false后就会有限制了
})
console.log(Person.name) //梵高先生
Person.name="莫扎特先生" //如果上面设置了writable为false后,属性value的值不会被修改
console.log(Person.name) //梵高先生
/*
访问器属性 _variable表示只能通过对象访问的属性
*/
var book={
_year:2004,
edition:1
}
Object.defineProperty(book,'year',{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue>2004){
this._year=newValue;
this.edition=this.edition+(newValue-2004);
}
}
})
book.year=2008;
console.log(book.edition) //5
定义多个属性defineProperties
/*
定义多个属性
Object.defineProperties(obj,{props}) 参数一为要修改属性的对象,参数二为要修改的属性对象
读取属性的特征
Object.getOwnPropertyDescriptor(obj,描述符对象的属性)
*/
var book={};
Object.defineProperties(book, {
_year:{
// writable:true,
value:2004
},
edition:{
writable:true,
value:1
},
year:{
get:function(){
return this._year
},
set:function(newValue){
if(newValue>2004){
this._year=newValue;
this.edition+=newValue-2004
}
}
}
})
book.year=2008;
console.log(book.edition) //5
var descriptor1=Object.getOwnPropertyDescriptor(book, "_year")
console.log(descriptor1)
/*
{value: 2004, writable: false, enumerable: false, configurable: false}
configurable: false
enumerable: false
value: 2004
writable: false
__proto__: Object
*/
var descriptor2=Object.getOwnPropertyDescriptor(book, "edition")
console.log(descriptor2)
/*
{value: 5, writable: true, enumerable: false, configurable: false}
configurable: false
enumerable: false
value: 5
writable: true
__proto__: Object
*/
var descriptor3=Object.getOwnPropertyDescriptor(book,"year")
console.log(descriptor3)
/**
{get: ƒ, set: ƒ, enumerable: false, configurable: false}
configurable: false
enumerable: false
get: ƒ ()
set: ƒ (newValue)
__proto__: Object
*/
2. 创建对象
对象字面量和Object()
var person=new Object();
person.name="cc"
//便于收编属性,
//方便函数传参调用
var person={
name:"cc"
}
3. 工厂模式(常见)
ECMA无法创建类(ES6支持Class),利用函数封装一特定接口创建对象的细节,虽然解决了创建对象的问题,但是没有解决如何知道对象的类型
/*
工厂模式:抽象了创建具体对象的一过程
缺点:无法知道一个对象的类型(对象识别的问题)
*/
function createPerson(name,age){
var obj= new Object();
obj.name=name;
obj.age=age;
obj.say=function(){
console.log(this.name)
}
return obj
}
var person=createPerson("梵高先生",20);
person.say()
4. 构造函数(常见)
- 没有显示的创建对象
- 直接将属性和方法赋给this
- 没有return对象
/*
使用构造函数会经历以下四个过程
1.创建一个新对象
2.将构造函数的作用域赋给新的对象 因此this就指向这个对象
3.执行构造函数中的代码 也就是给新对象添加属性
4.返回新的对象
缺点:
1.每个方法都要在不同的对象实例中创建一次,造成不必要的资源浪费
(函数也是对象Function,每次new的时候都会创建一次)
*/
function Person(name,age,food){
this.name=name;
this.age=age;
this.food=food;
this.say=function(){ //每次创建实例都会调用,造成不必要的资源浪费
console.log(this.name)
}
this.eat=eat; //会导致定义多个全局函数
}
/*
obj1和obj2共享一个全局作用域中的函数eat
问题思考:调用时候是被对象调用,全局作用域名副其实,会导致定义多个全局函数
*/
function eat(){
console.log(this.food)
}
var obj1=new Person("我是对象一",10,'米粉');
var obj2=new Person("我是对象二",20,'花甲')
obj1.eat()
obj2.eat()
console.log(obj2.constructor===Person)
/*
问题详细补充:
1.把构造函数当做函数,
- 也就是说任何函数只要通过new操作符调用,我们都可以称之为构造函数
- 如果不使用new作为普通函数调用,则调用结果会指向全局window对象
- 在另一个对象作用域中调用 Person.call(实例,属性1,属性2....)
2. 构造函数的问题
this.sayName=new Function() 以这种方式创建函数,会导致不同的作用域链和标识符解析。创建机制相同,所以不同实例的sayName是不相等的,所以开销也比较大,我们可以在外部定义方法(我们将对象方法写在了全局作用域中),同样带来了问题,在全局中的方法实际是被某个对象调用,这让全局作用域有点 对不上号,如果需要定义很多方法,就要定义很多全局函数,所以面向对象丝毫没有封装可言,所以我们可以通过下一节的原型模式解决
*/
5.原型模式(常见+重点)
每个函数都有一个原型属性prototype,这个属性是一个指针,指向一个对象
这个对象包含有特定类型的所有实例共享的属性和方法
理解原型对象
/*
1. 原型对象默认会拥有constructor属性指向构造函数 Person.prototype.constructor==Person //true
2. __proto__ 隐式属性 存在实例和构造函数的原型对象之间 ,而不是实例和构造函数之间
p.__proto__==Person.prototype //true
3. 一旦给实例设置了属性,就会阻止我们访问原型对象中的同名属性
一旦使用delete删除了属性,就会恢复原型对象的链接
*/
function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
this.sayName=function(){
console.log(this.name,this.sex,this.sex);
}
}
Person.prototype.address="aaa"
var p=new Person('cc',18,'男');
p.address="bbb" //就会阻止我们访问原型对象中的同名属性
console.log(p.addesss) //bbb而不是aaa
delete p.address;
console.log(p.addesss) //恢复原型对象的链接 aaa
/*
换句话说,添加的属性只会阻止我们访问原型中的属性,但不会修改那个属性,即使是添加属性设置为null,,也 只在实例中设置,并不会恢复指向原型的链接,我们可以使用delete完全删除实例属性
*/
原型与in操作符
-
in操作符
//确定一个属性是原型属性 function hasPrototypeProperty(object,name){ return !object.hasOwnProperty(name) && (name in object) } var person=new Person("cc"); Person.prototype.id=123 hasPrototypeProperty(person,'id') // 存在原型中
-
for in
返回所有能够通过对象访问和可枚举的属性,其中包括存在实例和原型中的属性
ie8中及更早版本,存在bug:屏蔽不可枚举属性的实例属性不会出现在for..in循环中
更简单的原型语法(重写原型)
前面每次添加原型属性都要使用 Person.prototype,每次都要写一遍,为了减少不必要的输入,我们可以通过对象字面量来重写原型对象(收编),从视觉上更好的封装原型的功能
注意点: 这种做法会导致 constructor属性不再指向Person了,因为原型对象被重写了,尽管instanceof能检查实例,但是constructor以及无法确定对象类型了
function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
this.sayName=function(){
console.log(this.name,this.sex,this.sex);
}
}
/*
原型对象被重写,constructor属性不再指向Person了,我们可以手动重置constructor
但是这种重设置会导致 [[Enumerable]]特性被设置为true,默认constructor属性是不可枚举的
*/
Person.prototype={
// constructor:Person,
address:"地址"
}
var p=new Person('cc',18,'男');
/*
如果设置兼容性则可以使用Object.defineProperty(),重设构造函数,只适用用ES5兼容的浏览器
*/
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false, //默认constructor属性是不可枚举的
value:Person
})
原型的动态性
在原型中查找值的过程是一次搜索,所以对原型对象做任何修改都会立即从实例上反映出来,
例:在生成实例代码后面(代码顺序),我们在原型对象上创建一个方法,结果依然能调用,因为实例和原型之间的松散连接关系
var p=new Person();//生成实例
Person.prototype.say=function(){ //生成实例代码后面添加
console.log(this.name)
}
p.say() //结果依然能调用
当我们调用构造函数创建实例时,js会给实例添加一个指向最初原型的 [[prototype]]指针,如果对象字面量重写原型,就切断了这个实例和原型的关系,而指向Object
原生对象的原型 Object,Array,String
不推荐修改原型以及扩展方法,会导致冲突,也有可能 重写原生对象的方法
原型模式的问题
问题: 由于原型对象的共享性导致的,函数(方法)可以使用,基本值类型也说得过去,但是使用引用类型值的属性时,问题就比较突出了 (原型对象中使用引用数据类型的问题 )
要共享相同基本数据(如数组)没什么问题,但是实例一般都是有自己单独的属性的,这也就是没人单独使用原型模式的问题的原因所在
6. 组合使用构造函数模式和原型模式(常见)
创建自定义类型最常见的方式就是 组合使用构造函数模式和原型模式 ,这是在ECMAScript中使用最为广泛,认同度最高的一种创建自定义类型的方法,也可以说是定义引用类型的一种默认模式
- 构造函数用于定义实例属性
- 原型模式用于定义方法和共享的属性
/*
好处:
1.每一个实例都会有自己的一份实例属性的副本,但同时有共享者对方法的引用,最大限度的节省了内存
2.这种混合模式还支持向构造函数中传递参数,可谓是集二者之长
*/
function Person(name,age,obj){
this.name=name;
this.age=age;
this.sex=obj;
this.friends=['cc01','cc02'];
}
Person.prototype={ //重写了原型Person.prototype,
constructor:Person, //需要手动重置constructor (Person.prototype.constructor==Person)
sayName:function(){
console.log(this.name)
}
}
var person01=new Person("张三",18,'前端开发');
var person02=new Person("李四",20,'java后台');
person01.friends.push("cc03");
console.log(person01.friends); // ["cc01", "cc02","cc03"]
console.log(person02.friends) // ["cc01", "cc02"]
7.动态原型模式(常见)
之前组合使用构造函数和原型模式时,二者都是分别单独定义的 。使用动态原型模式可以将所有信息都封装在构造函数中的,而通过在构造函数中初始化原型(必要情况下),又同时使用构造函数和原型的优点
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
if(typeof this.sayName != 'function'){ //添加方法
Person.prototype.sayName=function(){
console.log(this.name)
//注意点: 使用此模式不能使用对象字面量重写原型对象,已经创建了实例的情况下然后去重写原型,会切断新的原型对象和实例之间的联系
}
}
}
var person=new Person("cc",18,'前端开发')
person.sayName() //cc
8.寄生构造函数模式
场景: 前面几种模式都不适用的情况下可以使用此模式
基本思想:创建一个函数,作用仅仅是封装创建对象的代码,然后返回新创建的对象
/*
注意点:
寄生构造函数模式,返回的对象和构造函数或者与构造函数的原型对象没有任何关系,也就是说
构造函数返回的对象与在构造函数之外创建的对象没有什么不同,
instanceof检测无意义,
所以由于这些问题,建议在使用其他模式的情况下不要使用这种模式
*/
function Person(name,age,job){
var o=new Object();
o.name=name;
o.age=age;
o.job=job;
o.sayName=function(){
console.log(this.name)
}
return o;
}
var person01=new Person("cc",18,'java')
// 在特殊场景下用来创建构造函数,假设增加一个数组方法,不能修改Array的构造函数情况下,我们可以自己使用这个模式创建
function SpecialArray(){
//创建数组
var values=new Array();
//添加值
values.push.apply(values,arguments); //用构造函数接受到的所有参数 这里使用arguments对象
//添加方法
values.toPipedString=function(){
return this.join("|")
}
//返回数组
return values
}
var colors=new SpecialArray('red','green','blue')
console.log(colors.toPipedString()) //red|green|blue
9.稳妥构造函数模式
所谓稳妥就是 没有公共属性,而且其方法也不引用this的对象 ,最适合在一些安全的环境中(会禁用this和new),或者在防止数据被其他应用程序改动时使用
/*
稳妥构造函数模式遵循与寄生构造函数模式类似的模式
不同之处:
1.新创建对象的实例不引用this
2.不适用new操作符调用构造函数
和寄生构造函数模式一样,返回的对象和构造函数或者与构造函数的原型对象没有任何关系, 所以instanceof操作符没意义
*/
function Person(name,sex,job){
var o=new Object();
//这里可以添加定义 私有变量和函数
o.name='cc'
o.sayName=function(){
console.log(name) //只有通过此方法才能访问到name
}
return o;
}
var person=Person("cc",12,'python') //person保存的是一个稳妥对象
person.sayName()
10. 继承(重点)
OOP预语言都支持两种方式的继承:
- 实现继承:是指继承实际的方法
- 接口继承 :只继承方法的签名,ECMAScript没有函数签名,所以只能支持实现继承
原型链继承
基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法
原型,构造函数,实例三者关系 :
- 每个构造函数都是一个原型对象,原型对象都包含一个指向构造函数的指针(constructor)
- 每个实例都包含一个指向原型对象的内部指针(proto)
function SuperType() {
this.property = "我是父类属性"
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.property = "我是子类属性"
}
/*
SubType继承了SuperType,实现继承的本质是重写原型对象,用新类型的实例代替(替换原型)
新原型拥有SuperType实例所拥有的所有属性和方法,其中内部指针__proto__也指向SuperType的原型对象
new SuperType()-->SubType.prototype-->SuperType.prototype
*/
SubType.prototype = new SuperType();
//添加新的方法
SubType.prototype.getSubProperty = function() {
return this.property; //实例属性和原型方法
}
//重写父类型中的方法
SuperType.prototype.getSuperValue = function() {
return false
}
var instance = new SubType();
console.log(instance) //SubType{property}
console.log(instance.__proto__) //SuperType{property,getSubProperty}
console.log(instance.constructor) //指向SuperType是因为SubType.prototype中的constructor被重写了
console.log(instance.getSubProperty()) //我是子类属性
console.log(instance.getSuperValue()) //false
总结:
-
别忘了默认的原型是Object,SubType继承了SuperType.SuperType又继承了Object
-
确定原型和实例的关系
-
谨慎定义方法 覆盖和添加方法时一定要放在替换原型的语句之后 ,也不要使用对象字面量创建原型的方法,
这样会重写原型链
-
原型链的问题
1.包含引用原型的原型属性会被所有实例共享,(为什么定义在构造函数中,而不再原型中定义属性) 2.创建子类型的实例时,不能向父类型的构造函数中传递参数,所以实际很少会单独使用原型链
借用构造函数
解决原型对象中包含引用类型值所带来的问题,使用一种叫做 借用构造函数的模式 (伪造对象或经典继承)
实现方式:在子类型构造函数的内部调用父类型构造函数 (记住:函数只不过是在特定环境中执行代码的对象),然后使用apply()和call()可以在将来创建的对象上执行构造函数
/*
好处:
相对与原型链来讲,借用构造函数很大的优势是可以 在子类型中构造函数中可以向父类型构造函数中传递参数
问题:
如果仅借用构造函数,那么就无法避免构造函数模式所存在的问题----方法都在构造函数中定义,函数的复用就无从谈起,所以很少是单独使用的
*/
function Person(name) {
this.colors = ['a', 'b', 'c'];
this.name = name //还可以通过call传递参数
}
function Student() {
Person.call(this, 'EastBoat')
// Person.apply(this, ['EastBoat'])
//表示在未来将要创建的新对象(实例)person01和person02的各自环境中调用Person的构造函数,各自都会有自己的副本了,互不影响,同时传递了参数
}
var person01 = new Student()
person01.colors.push("fff", 'ggg');
console.log(person01.colors) //["a", "b", "c", "fff", "ggg"]
console.log(person01.name)
var person02 = new Student();
console.log(person02.colors) //["a", "b", "c"]
console.log(person02.name)
组合继承
也叫作伪经典继承:将原型链和构造函数组合在一起使用,从而发挥二者之长的一种继承模式
/*
思想: 使用原型链实现对原型属性和方法的继承,
通过借用构造函数实现对实例属性的继承
作用: 通过在原型对象上定义方法实现了函数复用,又能保证每个实例都有他自己的属性
优点: 避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为javascript中最常用的继承模式
instanceof和isPrototypeOf()也能够识别基于组合继承创建的对象
缺点 无论什么情况下,都会调用两次父类型的构造函数,
第一次:创建子类的原型的时候 SubType.prototype=new SuperType()
第二次:在子类的构造函数内部 SuperType.call(this)
第一次调用SubType.prototype会得到两个属性,本身他们是SupperType的实例属性,现在位于子类原型SubType.prototype中
第二次调用SubType构造函数时----call(this),此时又会调用SuperType构造函数,此时生成SubType属性,就自动屏蔽了第一次SubType的原型
*/
function SuperType(name, age) {
this.name = name;
this.age = age;
this.hobby = ['篮球', '唱歌'];
}
SuperType.prototype.sayHobby = function() {
console.log(this.hobby)
}
function SubType(name, age) {
SuperType.call(this, name) //继承父类属性
this.age = age;
}
SubType.prototype = new SuperType(); //继承方法
SubType.prototype.sayAge = function() {
console.log("我是SubType的原型方法" + this.age)
}
var p1 = new SubType('p1', 22);
p1.hobby.push("羽毛球")
p1.sayHobby();
p1.sayAge()
var p2 = new SubType("p1", 18);
console.log(p2.hobby);
p2.sayHobby();
p2.sayAge()
原型式继承
没有使用严格意义上的构造函数,借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
整个过程和Object.create(person)一样,是创建了对象的一个副本,
这种方式比较少 ,原因就是和原型链继承一样,引用数据类型容易改变。
function object(o) {
function F() {} //临时性构造函数
F.prototype = o; //传入的对象作为这个构造函数的原型
return new F() //返回临时类型的一个新实例
}
/*
本质上,object()对传入其中的对象执行了一次浅复制,
要求你必须有一个对象可以作为另一个对象的基础,
下面的案例中,可以作为另一个对象基础的是Person对象,我们将其传入到object函数中,然后就返回出一个新的对象
*/
var person={
name:"cc",
food:['a','b','c']
}
var p1=object(person)
p1.name='p1';
p1.food.push('dddd');
console.log(person.food); //["a", "b", "c", "dddd"]
var p2=object(person);
p2.name="p2";
p2.food.push("eeee")
console.log(person.food); //["a", "b", "c", "dddd", "eeee"]
寄生式继承
寄生式继承在原型式继承的基础上增加了自己的方法。
/*
与原型式继承密切相关的一种思路
类似: 寄生构造函数和工厂模式
缺点: 为对象添加函数,由于无法做到函数复用而降低效率,这一点和构造函数模式类似
*/
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function createAnother(original) {
//调用函数创建一个新对象,此处object不是必须的,任何能够返回新对象的函数都适用此模式
var cloneObj = object(original);
cloneObj.sayHi = function() {
console.log('hihihiih')
}
return cloneObj;
}
var person = {
name: "cc",
hobby: ['a', 'b', 'c']
}
var anotherPerson = createAnother(person);
console.log(anotherPerson) //hihihiih
anotherPerson.sayHi()
寄生组合式继承(最理想 )
组合继承并不是最好的继承,因为通过将构造函数的原型指向父构造函数的实例,会两次调用构造函数 ,前面已经讲了组合继承的缺点,我们不必为了指定子类的原型对象而去调用父类的构造函数,我们无非只需要父类型的一个副本而已,
优点:
- 高效体现在这种模式只调用了一次superType构造函数,并且因此避免了在SubType.prototype上创建不必要的,多余的属性
- 与此同时,还能保证原型链不变,还能够正常使用instanceof和isPrototypeOf()
/*
解决组合继承中两次调用superType构造函数的问题,其方法是在中间架一座桥梁(加一个空的构造函数)
另外不能直接将SubType.prototype指向SuperType.prototype是因为会引用相同的内存,造成共享,所以要指向实例。
实际过程如下: 中间桥梁为空的构造函数,后面注释为object()函数的实现过程,也可使用Object.create()
function Temp(){ } // function F() { }
Temp.prototype=SuperType.prototype; // F.prototype = SuperType.prototype
var temp=new Temp(); // var prototype=new F(); ///创建对象
temp.constructor=SubType // prototype.constructor = SubType; //增强对象
SubType.prototype=temp; // SubType.prototype = prototype //指定对象
*/
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); //创建对象
//var prototype = Object.create(superType.prototype); //ES5新增方法
prototype.constructor = subType; //增强对象
subType.prototype = prototype //指定对象
}
//父类构造器
function SuperType(name) {
this.name = name;
this.colors = ['res', 'yellow', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
//子类构造器
function SubType(name, age) {
this.name = name;
this.age = age;
}
inheritPrototype(SubType, SuperType) //继承实现
SubType.prototype.sayAge = function () {
console.log(this.age)
}
//重写原型对象方法sayName
//SubType.prototype.sayName = function () {
// console.log("bb")
//}
var p = new SubType("cc", 18);
p.sayAge(); //18
p.sayName()//cc 如果重写原型对象方法,打印就是'bb'