/** * JS 对象及原型 * * 对象是一种复合值,可看作是属性的无序集,每个属性都是一个名/值对 * 对象是动态的,可增加和删除属性,不能存在两个同名的属性 * 对象最常用的用法是:创建(create)、设置(set)、查找(query)、删除(delete)、检测(test)、枚举(enumerate) * * 对象的属性: * * 1、数据属性: * 值(value):属性的值 * 可写(writalbe attribute):表明是否可以设置该数据属性的值 * 可枚举(enumerable attribute):表明是否可以通过for/in循环返回该数据属性 * 可配置(configurable attribute):表明是否可以删除或修改该数据属性,或者能否把该数据属性修改为访问器属性 * * 2、存取器属性: * getter:当程序查询存取器属性时调用 getter 方法,默认值为undefined * setter:当程序设置存取器属性时调用 setter 方法,默认值为undefined * 可枚举(enumerable attribute):表明是否可以通过for/in循环返回该存取器属性 * 可配置(configurable attribute):表明是否可以删除或修改该存取器属性 * * 除了对象的属性外,另外还有三个与对象相关的对象特性: * 对象的原型(prototype):指向另一个对象,本对象的属性继承自它的原型对象 * 对象的类(class):是一个标识对象类型的字符串 * 对象的扩展标记(extensible flag):指明是否可以向该对象添加新属性 * * 原型 * 所有通过对象直接量创建的对象都具有同一个原型 * 通过关键字 new 和构造函数创建的对象的原型就是构造函数的 prototype 属性的值 * Object.prototype 没有原型,它不继承任何属性。其他原型对象都是普通对象,普通对象都具有原型。 * 所有的内置构造函数都具有一个继承自 Object.prototype的原型 */ /** * 对象的创建 * 对象直接量、工厂模式、构造函数模式、原型模式、使用 Object.create() 方法 */
//1 工厂模式,优:简单、可复用,缺:不能确定对象属于哪个类
//2 原型链模式,优:可以共享属性和方法,缺:(不想被共享的)引用类型会被共享,给超类传递参数时会影响所有实例
//3 构造函数模式,优:创建的实例可以标志为一种特定的类型,缺:函数不能复用(每个函数对应第一个实例,并且若把函数放在全局作用域中就没有了封装性)、
//子类不能获取超类原型中的属性和方法
//对象直接量(对象直接量是表达式,即每次运算创建的都是一个新的对象) //通过直接量创建的对象使用 Object.prototype 作为原型 var obj={name:"guang",sum:3+3,obj:{"":""}}; //工厂模式 function createPerson(name){ var obj = new Object(); obj.name = name; obj.sayName = function(){ console.log(this.name); }; return obj; } var obj=createPerson("guang");
//优点:可复用
//缺点:不能确定生成的对象属于哪个类 //构造函数模式 //原生构造函数(如:Object 和Array 等,在运行时会自动出现在执行环境中) //通过 new 创建的对象使用构造函数的 prototype 属性作为原型 var obj=new Object(); //自定义构造函数(按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头) //创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方 /** * 以这种方式调用构造函数实际上会经历以下4个步骤: * (1) 创建一个新对象 * (2) 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象) * (3) 执行构造函数中的代码(为这个新对象添加属性) * (4) 返回这个新对象 */ function Person(name){ this.name = name; this.sayName = function(){ console.log(this.name); }; } var obj=new Person("guang"); //构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new 操作符来调用,那它就可以作为构造函数; //而任何函数,如果不通过new 操作符来调用,那它跟普通函数也不会有什么两样,如果上面的函数作为普通函数来调用: // Person("guang"); // 添加到 window(node 环境下添加到 global) // window.sayName(); //"guang" //原型模式 function Person(){/* code */} Person.prototype = { name : "guang", sayName : function () { console.log(this.name); } }; var obj=new Person(); //通过 Object.create()创建对象(该方法是静态方法) //参数:参数一:要创建的对象的原型 参数二(可选):用以对对象属性进行进一步描述 var obj=Object.create({name:"guang"});//继承了属性 name var obj=Object.create(null);//没有继承任何东西 var obj=Object.create(Object.prototype);//创建普通空对象(和 {} 或 new Object()一样)
/** * 对象的数据属性 * 通过点运算符.访问对象的属性时,属性名用一个标志符来表示,标识符不是数据类型,因此无法动态修改 * 通过括号[]访问对象的属性时,属性名通过字符串来表示,字符串是JS数据类型,因此可以动态的修改和创建 */ var obj={}; function addName(name,value){ obj[name]=value;//不能使用 obj.name=value; 因为obj对象还不存在该属性 } //在js中,对于非引用类型,如果对象允许属性赋值操作,它 总是 在原始对象上创建属性或对已有的属性赋值,而不会去修改原型链 var obj1={name:"obj1"},obj2=Object.create(obj1),obj3={name:"obj3"}; console.log(obj1,obj2,obj3);//{ name: 'obj1' } {} { name: 'obj3' } obj2["name"]="GUANG",obj3["name"]="guang"; console.log(obj1,obj2,obj3);//{ name: 'obj1' } { name: 'GUANG' } { name: 'guang' } // 可见obj2创建了"name"属性,obj3对自己已有的属性进行赋值,两者都没有修改原型链 //对于引用类型,则会影响原型链 var obj1={name:"obj1",arr:[1,2]},obj2=Object.create(obj1); console.log(obj1,obj2);//{name: 'obj1', arr: [ 1, 2 ]} {} obj2.arr.push(3); console.log(obj1,obj2);//{name: 'obj1', arr: [ 1, 2, 3 ]} {} // 可见 obj2.arr 引用的是原型对象 obj1.arr ,操作时修改了原型链
/** * 属性检测 * in:该操作符的左侧为属性名(字符串),右侧为对象,若对象(右侧)的自有属性或者继承属性包含这个属性(左侧)则返回true * hasOwnProperty():检测属性是否为对象的自有属性,是则返回true,继承属性返回false * propertyIsEnumerable():除了满足 hasOwnProperty() 的要求外,还需要属性可枚举,否则返回false */ var obj=Object.create({name:"guang"});//obj继承"name"属性 console.log("name" in obj);//true console.log(obj.hasOwnProperty("name"));//false Object.defineProperty(obj,"x",{ value: 666, enumerable: true});//给 obj 添加一个可枚举的属性 x:666 console.log(obj.propertyIsEnumerable("x"));//true Object.defineProperty(obj,"y",{ value: 888, enumerable: false});//给 obj 添加一个不可枚举的属性 x:666 console.log(obj.propertyIsEnumerable("y"));//false
/** * 属性的枚举 * for/in 循环可遍历对象中所有可枚举的属性(包括自有的和继承的属性) * Object.keys() 方法接收一个对象作为参数,返回一个包含该对象所有可枚举属性的字符串数组 * Object.getOwnPropertyNames() 方法接收一个对象作为参数,返回一个包含该对象所有属性的字符串数组 * 对象继承的内置方法不可枚举,而代码中给对象添加的属性可以枚举(除非主动将该属性转为不可枚举) */ var obj1={obj1_prop:"hello"},obj2=Object.create(obj1);//继承 obj1 的 hello 属性 obj2["obj2_prop"]="world",Object.defineProperty(obj2,"name",{value:"guang",enumerable:false});//定义自身可枚举和不可枚举的属性 for(var prop in obj2){ console.log(prop,obj2[prop]); } //obj2_prop world //obj1_prop hello console.log(Object.keys(obj2));//[ 'obj2_prop' ] console.log(Object.getOwnPropertyNames(obj2));//[ 'obj2_prop', 'name' ]
/** * 对象的存取器属性 * 存取器属性的特性 getter、setter、enumerable attribute、configurable attribute * 对象属性由名/值对构成,属性值还可以用一个或两个方法替代(getter/setter),由这两个方法定义的属性称为"存取器属性"(accessor property) */ var obj={ $num:0,//或者 _num:0 , _ 或者 $ 符号暗示该属性通过对象方法访问,这是个私有属性(没什么特别含义,这是一种习惯而已) get selfPlus(){return ++this.$num},//存取器属性的 getter 特性 set selfPlus(num){this.$num=num>this.$num?num:this.$num;}//存取器属性的 setter 特性 } console.log(obj.selfPlus,obj.selfPlus);//1(自增1) 2(自增1) obj.selfPlus=0; //0<2,设置失败,$num 依然为2 console.log(obj.selfPlus);//3(自增1) obj.selfPlus=5; //5>2,设置成功,$num 变为5 console.log(obj.selfPlus);//6(自增1)
/** * 对象的方法 * Object.getOwnPropertyDescriptor()、Object.defineProperty()、Object.defineProperties()、 * Object.getPrototypeOf()、toString()、valueOf() */ //Object.getOwnPropertyDescriptor()方法,可以取得给定的自有属性的描述符。 //参数一:属性所在的对象 参数二:要读取其描述符的属性名称 。返回一个对象(对象中包含属性的4个特征) console.log(Object.getOwnPropertyDescriptor({x:1},"x")); //{ value: 1, writable: true, enumerable: true, configurable: true } console.log(Object.getOwnPropertyDescriptor(obj,"selfPlus")); //{ get: [Function: get selfPlus],set: [Function: set selfPlus],enumerable: true,configurable: true } //继承属性、不存在的属性、空属性,都返回 undefined console.log(Object.getOwnPropertyDescriptor({},"x"));//undefined console.log(Object.getOwnPropertyDescriptor({},"x"));//undefined console.log(Object.getOwnPropertyDescriptor({},"toString"));//undefined //Object.defineProperty()方法,用于设置属性的特征或新建具有某种特征的属性 //参数一:要修改的对象 参数二:要设置或创建的属性 参数三:属性描述符对象 //默认值:writable/enumeralbe/configurable 默认为: false var obj={x:1}; Object.defineProperty(obj,"x",{ value: 666, writable: true, enumerable: true, configurable: true }); console.log(obj.x);//666 设置属性的特征(把值设为 666) Object.defineProperty(obj,"name",{ get:function(){},set:function(param){},enumerable:true,configurable:true }); console.log(obj);//Object { x: 1, name: Getter & Setter } 新建具有某种特征的属性(新建一个具有上述特征的属性 name) //配置多个属性 Object.defineProperties({},{ x:{value:1,writable:true,enumerable:false,configurable:true}, y:{value:2,writable:false,enumerable:false,configurable:true}, z:{get:function(){},configurable:true} }); /** * Object.defineProperty()或Object.defineProperties()违反下列规则会抛出类型错误异常 * 1、若对象是不可扩展的,则可以编辑已有的自有属性,但不能给它添加新属性 * 2、若睡醒是不可配置的,则不能修改它的可配置性和可枚举性 * 3、若存取器属性是不可配置的,则不能修改getter、setter方法,也不能将它转换为数据属性 * 4、若数据属性是不可配置的,则不能将它转换为存取器属性 * 5、若数据属性是不可配置的,则不能将它的可写性从false改为true,但可以从true改为false * 6、若数据属性是不可配置且不可写的,则不能修改它的值,然而可配置但不可写属性的值是可以修改的(修改为可写-修改值-修改为不可写) */ /** * Object.getPrototypeOf() 查询原型属性 */ var obj=Object.create({x:1}); console.log(Object.getPrototypeOf(obj)); //{ x: 1 }
/** * toString() 方法返回一个表示调用这个方法的对象值的字符串 */ var obj={name:"guang"}; console.log(obj.toString());//[object Object] /** * valueOf() 需要将对象转换为原始值的时候调用该方法 */ var date=new Date(2049,10,1); console.log(date,date.valueOf());//2049-10-31T16:00:00.000Z(date) ,2519308800000(date.valueOf())
/** * 继承 * 通过原型链、借用构造函数、组合继承 */ //通过原型链(js原型以及原型链) function Parent(){ this.parentProperty="I am your father ,ha ha ha !" } function Child(){} Child.prototype=new Parent();//Child 函数的原型是 Parent 函数原型的一个实例 var obj=new Child();//创建一个 Child 实例 console.log(obj.parentProperty); //I am your father ,ha ha ha ! 可以看到 obj 实例通过原型链继承了 parentProperty 属性
//原型继承的缺点:其中,1.最主要的问题来自包含引用类型值的原型。2.在创建子类型的实例时,不能向超类型的构造函数中传递参数。 //实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。--《JavaScript高级程序设计》 //对于第一点可用如下例子说明 function Parent(){ this.friends=["A"]; } function Child(){} Child.prototype=new Parent(); var obj1=new Child(); obj1["friends"].push("B");//obj1 添加了一个朋友 B var obj2=new Child(); console.log(obj1,obj2);//Parent {} Parent {} /** * 解析:使用 new Child()创建实例时,结合当前这个例子,调用构造函数实际上会经历以下4个步骤: * (1) 创建一个新对象 ,[ 由于 Child的原型是 Parent的实例,因此这个空对象是 Parent原型的实例: Parent {} ] * (2) 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象) * (3) 执行构造函数中的代码(为这个新对象添加属性),[ 由于 Child()函数中没有代码执行,所以执行完这一步后还是: Parent {} ] * (4) 返回这个新对象 ,[ 即返回对象: Parent {} ] */ console.log(obj1.friends,obj2.friends); //[ 'A', 'B' ] [ 'A', 'B' ] obj2 也"被"添加了一个朋友 B,这是因为 Child的原型(Parent{}实例)中有 friends数组
//对于第二点说明
//在创建对象过程在,传达函数有两种途径,(1)Child.prototype=new Parent(参数); (2)var obj1=new Child(参数);
//但是 (1) 会影响所有实例 ,(2) (在没有和构造函数的方法结合使用的情况下)参数传递不到超类中
//借用构造函数
//在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数 (constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。
//这种技术的基本思想相当简单,即 在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,
//因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数
function Parent(name){ this.friends=["A"]; this.name=name; } function Child(name){ Parent.call(this,name);//继承 Parent } var obj1=new Child("obj1");//可以传递参数 obj1["friends"].push("B");//obj1 添加了一个朋友 B var obj2=new Child("obj2");//可以传递参数 console.log(obj1,obj2); //Child{ friends: [ 'A', 'B' ], name: 'obj1' } Child{ friends: [ 'A' ], name: 'obj2' } /** * 解析:使用 new Child()创建实例时,结合当前这个例子,调用构造函数实际上会经历以下4个步骤: * (1) 创建一个新对象 ,[通过new调用 Child构造函数 生成 Child原型的实例: Child{} ] * (2) 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象) * (3) 执行构造函数中的代码(为这个新对象添加属性),[ 执行 Child(name)函数中的代码: Parent.call(this,name); * 此时上面那个空的实例(Child{})就是 Parent(name)构造函数中的this(因为通过apply调用),然后执行 Parent(name)构造函数的代码 * 得到一个有 friends、name 属性的 Child实例: Child{ friends: [ 'A', 'B' ], name: 'obj1/obj2' }] * (4) 返回这个新对象 ,[ 即返回对象: Child{ friends: [ 'A', 'B' ], name: 'obj1/obj2' } ] */
//使用构造函数继承的缺点: //1.方法都在构造函数中定义,因此函数复用就无从谈起了。2.而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式 //对于第一点可用如下例子说明 //对于前一个 Parent函数来说,如果该函数有一个用于显示名字的方法,如下: function Parent(name){ this.friends=["A"]; this.name=name; this.sayName=function(){return this.name}; } //那么可以看到,每一个实例都有一个自己的方法,如果创建一百个实例,那么就有一百个独立的 sayName()方法 console.log(obj1,obj2); //Parent { friends: [ 'A', 'B' ], name: 'obj1', sayName: [Function] } Parent { friends: [ 'A' ], name: 'obj2', sayName: [Function] }
//对于第二点说明
//由于利用构造函数创建对象时,子类只是像调用普通函数一样调用超类的方法(调用完成后,子类并没有任何相关的指针指向超类或超类原型),
//因此只能获取到超类函数中的属性和方法(函数的普通调用),而超类原型中的属性和方法子类无法获取
//组合继承 //结合 原型链继承 和使用构造函数继承 的优点,组合使用 //这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。 function Parent(name){ this.friends=["A"]; this.name=name; } function Child(name){ Parent.call(this,name);//继承 Parent } Parent.prototype.sayName=function(){return this.name};//继承方法 Child.prototype=new Parent(); var obj1=new Child("obj1");//可以传递参数 obj1["friends"].push("B");//obj1 添加了一个朋友 B var obj2=new Child("obj2");//可以传递参数 console.log(obj1,obj2);//Parent { friends: [ 'A', 'B' ], name: 'obj1' } Parent { friends: [ 'A' ], name: 'obj2' } console.log(obj1.sayName(),obj2.sayName());//obj1 obj2
// ... // 原型式继承 // 寄生式继承 // 寄生组合继承