//早期建立对象的方式 var jonslike = new Object(); jonslike.name = "jon"; jonslike.like = "wow"; jonslike.saylike = function(){ console.log(this.name); }; //对象字面量建立方式 var jonslike = { name : "jon", like : "wow", saylike : function(){ console.log(jonslike.like); //亦可换成this, 由于指向当前对象 console.log(this.like); //wow } };
ES的两种属性, 数据属性和访问器属性javascript
数据属性包含1个数据值的位置, 这个位置能够读取和写入值java
名称 | 描述 |
---|---|
[[ Configurable ]] | 表示可否经过delete删除属性而从新定义属性, 可否修改属性的特性, 或者可否把属性修改成访问器属性, 默认值true |
[[ Enumerable ]] | 表示可否经过for-in循环返回属性, 默认值true |
[[ Writable ]] | 表示可否修改属性的值, 默认值true |
[[ Value ]] | 包含这个属性的数据值. 读取属性值的时候, 从这个位置读; 写入属性值的时候, 把新值保存在这个位置, 默认值undefined |
var person = { name : Jon, //value值变成Jon };
Object.defineProperty()
方法.接收3个参数, 属性所在的对象, 属性的名字, 一个描述符对象数组
描述符对象的属性必须是 : configurable, enumerable, writable, value浏览器
//设置为不可写 : var person = {}; person.name = "fire"; Object.defineProperty(person, "name", { writable : false, //设置为只读, 不可写 value : "jon" }); person.name = "mark"; alert(person.name); //输出仍是jon
//设置为不可配置 : var person = {}; Object.defineProperty(person, "name" , { configurable : false, //设置为不可配置 value : "jon", }); person.name = "mark"; //无效! delete person.name; //删除无效! alert(person.name); //依然能输出jon //NOTE : 配置configurable为false时, 其余3各特性也有相应的限制
访问器属性不包括数据值;安全
包含一对getter 与 setter 函数 (非必须)闭包
读取访问器属性时, 会调用getter函数,该函数负责返回有效的值;app
写入访问器属性时, 会调用setter函数,该函数负责决定如何处理数据;函数
名称 | 描述 |
---|---|
[[ Configurable ]] | 表示可否经过delete删除属性而从新定义属性, 可否修改属性的特性, 或者可否把属性修改成访问器属性, 默认值true |
[[ Enumerable ]] | 表示可否经过for-in循环返回属性, 默认值true |
[[ Get ]] | 在读取属性时调用的函数, 默认值undefined |
[[ Set ]] | 在写入属性时调用的函数, 默认值undefined |
定义访问器属性 : 使用Object.defineProperty()
方法.测试
//建立book对象, var book = { //定义两个默认的属性, _year和edition, 下划线定义的属性表示只能经过对象方法访问 _year : 2004, edition : 1 }; Object.defineProperty(book, "year", { get : function(){ return this._year; }, set : function(){ if(newValue > 2004){ this._year = newValue; this.edition += newValue - 2004; } }, }); book.year = 2005; alert(book.edition); //2
定义多个属性 : defineProperties()
ui
能够经过描述符一次过定义多个属性
接收两个参数 :
要添加和修改其属性的对象
第二个对象的属性与第一个对象中要添加或修改的属性一一对应.
var book = {}; Object.defineProperties(book, { _year : { value : 2004, }, edition : { value : 1, }, year : { get : funciton(){ return this._year, }, set : function(){ if(newValue > 2004){ this._year = newValue, this.edition += newValue - 2004, } } } });
读取属性的特性 : 使用Object.getOwnPropertyDescriptor()
;
能够取得给定属性的描述符
接收两个参数 :
属性所在的对象, 要读取其描述符的属性名称
返回值 : 一个对象, 若是返回的对象是访问器属性, 则这个对象的属性有configurable, enumerable, get, set; 若是返回的对象是数据属性, 则这个对象的属性有configurable, enumerable, writable, value
var book = {}; Object.defineProperties(book, { _year : { value : 2004, }, edition : { value : 1, }, year : { get : funciton(){ return this._year, }, set : function(){ if(newValue > 2004){ this._year = newValue, this.edition += newValue - 2004, } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); //数据属性 alert(descriptor.value); //2004(最初的值) alert(descriptor.configurable); //false(最初的值) alert(typeof descriptor.get); // undefined var descriptor = Object.getOwnPropertyDescriptor(book, year); //访问器属性 alert(descriptor.value); // undefined(访问器没有value属性) alert(descriptor.enumerable); // false alert(typeof descriptor.get); // function(一个指向getter的指针)
工厂模式抽象了建立具体对象的过程;
该模式没有解决对象的识别问题(即怎样知道一个对象的类型)
function createPerson(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayJob = function(){ console.log(this.job); }; return o; } var p1 = createPerson("Jon",25,"FrontEnd Developer"); var p2 = createPerson("Mark",24,"DBA"); p1.sayJob(); //FrontEnd Developer p2.sayJob(); //DBA
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayJob = function(){ console.log(this.job); }; } //使用new操做符建立Person的新实例 /* 调用构造函数会经历如下步骤 : 1. 建立一个新对象; 2.将构造函数的做用域赋给新对象(所以this就指向了这个新对象) 3.执行构造函数中的代码(为这个新对象添加属性) 4.返回新对象 */ var p1 = new Person("Jon", 25, "FrontEnd Developer"); var p2 = new Person("Mark", 24, "DBA"); p1.sayJob(); //FrontEnd Developer p2.sayJob(); //DBA //新对象具备一个constructor(构造函数)属性, 指向原建立的构造函数(即Person) console.log(p1.constructor == Person); //true console.log(p2.constructor == Person); //true //使用instanceof操做符检测对象类型会更可靠 console.log(p1 instanceof Object); //Object是终极父类, 因此返回true console.log(p1 instanceof Person); //p1是Person构造函数的实例 console.log(p2 instanceof Object); //Object是终极父类, 因此返回true console.log(p2 instanceof Person); //p2是Person构造函数的实例 //构造函数自己也是函数, 因此能够当作普通函数来调用(不使用new操做符调用) Person("Martin", 27, "PHPer"); //添加到window对象(全局做用域中) window.sayJob(); //PHPer //在另外一个对象的做用域调用(使用call()或者apply()) var o1 = new Object(); Person.call(o1, "Kiki", 23, "Singer"); o1.sayJob(); //Singer
每一个函数都有一个prototype
(原型)属性, 是一个指针, 指向一个对象
对象的用途是包含能够由特定类型的全部实例共享的属性和方法;
prototype
就是经过调用构造函数而建立的那个对象实例的对象
使用原型对象的好处是可让全部对象实例共享它所包含的属性和方法;
即 :
没必要在构造函数中定义对象实例的信息, 而是能够将这些信息直接添加到原型对象中;
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "f2e"; Person.prototype.sayName = function(){ alert(this.name); }; var p1 = new Person(); p1.sayName(); //jon var p2 = new Person(); p2.sayName(); //jon alert(p1.sayName == p2.sayName); //true
不管什么时候, 只要建立了一个新函数, 就会根据一组特定的规则为该函数建立一个prototype
属性, 该属性指向函数的 原型对象
即 : (新函数会建立一个prototype属性指向原型对象)
默认状况下, 全部原型对象会自动得到一个constructor
构造函数属性, 该属性包含指向prototype
属性所在函数的指针
即 : (全部原型对象得到一个constructor(构造函数)属性,包含指向prototype属性所在函数的指针)
function Person(){}; //这是(空)构造函数,会有一个prototype属性,指向(下面的)原型对象 //Person.prototype : 这是(构造函数的)原型对象, 会自动得到一个constructor(构造函数)属性, 包含一个指向prototype属性所在函数的指针,在这里即上面的Person()函数; 即Person.prototype.constructor指向(上面的)Person()函数 //下面这些是(构造函数的)原型对象的自定义属性s Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "f2e"; Person.prototype.sayName = function(){ alert(this.name); }; //这是实例,内部包含一个指针(内部属性) [[Prototype]], 指向构造函数的原型对象(即上面的Person.prototype) var p1 = new Person(); p1.sayName(); //jon var p2 = new Person(); p2.sayName(); //jon alert(p1.sayName == p2.sayName); //true
isPrototypeOf()
: 肯定是否为给定实例的原型
getPrototypeOf() [ES5]
: 跟上面的功能同样, 而且这方法能够返回原型对象给定属性的值
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); p1.sayJob(); //FrontEnd Developer //测试Person是否为p1的原型 console.log(Person.prototype.isPrototypeOf(p1)); //true //若是支持ES5的getPrototypeOf() if(Object.getPrototypeOf){ //测试Person是否为p1的原型 console.log(Object.getPrototypeOf(p1) == Person.prototype); //true //输出p1的name属性的值 console.log(Object.getPrototypeOf(p1).name); //Jon }
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); p1.job = "DBA"; p1.sayJob(); //DBA
delete
操做符能够删除实例的属性
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); p1.job = "DBA"; p1.sayJob(); //返回自身添加的属性, DBA delete p1.job; //删除p1的job属性 p1.sayJob(); //返回原型的属性, FrontEnd Developer
hasOwnProperty()
能够检查一个属性是位于实例仍是原型中, 属于实例会返回true
in
操做符会在对象能访问给定属性时返回true
,不管是实例仍是原型 : (就是有这个属性就会返回true)
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); console.log(p1.hasOwnProperty("name")); //实例中没有本身定义的name属性, 返回false console.log("name" in p1); //true, p1中有name属性(从Person中的name继承而来的) p1.name = "Mark"; //本身定义一个实例中的name属性, 覆盖原型继承而来的name console.log(p1.hasOwnProperty("name")); //实例中有本身定义的name属性(Mark), 返回true console.log("name" in p1); //true, p1中有name属性(从Person中的name继承而来的) delete p1.name; //删除p1实例的name属性 console.log(p1.hasOwnProperty("name")); //p1的name属性已经被delete操做符删除, 因此如今又没了自身实例的name属性, 因此返回false console.log("name" in p1); //true, p1中有name属性(从Person中的name继承而来的)
能够同时使用hasOwnProperty()
和in
操做符, 以肯定给定的属性是位于实例仍是原型中 :
in
操做符只要能访问给定属性就返回true
, hasOwnProperty()
只在属性属于实例才返回true
,
所以只要in
操做符返回true
而hasOwnProperty()
返回false
, 就能肯定给定的属性是原型的属性
//obj表示要传入的实例名称, name表示要测试的实例属性 function hasPrototypeProperty(obj, name){ //若是传入的实例属性name不属于该实例obj(取反), 而且(&&)实例obj中有该传入的属性name, 则返回 return !obj.hasOwnProperty(name) && (name in obj); } function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(hasPrototypeProperty(p1, "name")); console.log(hasPrototypeProperty(p1, "job")); //p1中尚未定义实例的job属性, 只使用了原型继承而来的job属性, 因此返回true (hasOwnProperty()返回!false(取反false, 即true), in操做符返回true) p1.job = "DBA"; //p1定义自身的实例属性job console.log(hasPrototypeProperty(p1, "job")); //false (!hasOwnProperty(job)为 !true,即false, in返回true)
使用for-in
返回能经过对象访问的, 可枚举的属性(包括原型内和实例内的) :
var o = { name : "Jon", age : 25, saySth : function(){} } for(var prop in o){ if(prop){ console.log(prop); } } //name, age, saySth
Object.keys() [ES5]
可得到全部可枚举的属性 :
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; //得到原型中全部可枚举的属性 var protoKeys = Object.keys(Person.prototype); console.log(protoKeys); //"name", "age", "job", "sayJob" var p1 = new Person; p1.name = "Mark"; p1.nickname = "MM"; p1.age = 24; p1.fakeAge = 21; p1.job = "DBA"; p1.sayJob(); //若是经过实例调用, 则会获得该实例中全部可枚举的属性 var keys = Object.keys(p1); console.log(keys); //"name", "nickname", "age", "fakeAge", "job"
Object.getOwnPropertyNames()
能够获得全部不管是否可枚举的属性
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; //得到原型中全部属性(不管是否可枚举) var protoKeys = Object.getOwnPropertyNames(Person.prototype); console.log(protoKeys); //"constructor", "name", "age", "job", "sayJob"
Object.keys()
和 Object.getOwnPropertyNames()
均可以替代for-in循环 (IE9+, ...)
使用对象字面量来建立新对象
function Person(){} //这种方式其实已经重写了默认的prototype对象, 此时constructor属性已经再也不指向Person了, 而是指向了Object Person.prototype = { name : "Jon", age : 25, job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //因此此时虽然instanceof操做符还能返回正确的结果, 但constructor已经没法肯定对象的类型了 var f1 = new Person(); console.log(f1 instanceof Person); //true console.log(f1 instanceof Object); //true console.log(f1.constructor == Person); //false console.log(f1.constructor == Object); //true //若是constructor的值很重要, 能够像这样把它设置回适当的值 //(修改上面的Person.prototype) Person.prototype = { constructor : Person, //显式的把constructor设置为Person name : "Jon", age : 25, job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //若是像上面同样把constructor的值显式的设置, 那么它会变成可枚举, 即[[Enumerable]]的值会变为true, 若是要把它设置回不可枚举, 可使用下面的ES5提供的新方法 : //重写整个示例 function Person(){} Person.prototype = { name : "Jon", age : 25, job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //重设构造函数[ES5 only] Object.defineProperty(Person.prototype, "constructor", { enumerable : false, value : Person });
原型的动态性
原型中查找值的方法是一次搜索, 所谓动态性就是在原型对象上全部的修改都能当即从实例上反应出来, 即便是 先建立实例, 后修改原型 也是如此
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; //建立原型实例 var p1 = new Person(); //建立实例后再建立原型方法 Person.prototype.sayAge = function(){ console.log(this.age); } //调用后建立的原型方法 p1.sayAge(); //照样能工做! 输出25
//但不能在建立原型实例后, 重写整个原型对象 function Person(){} //建立原型实例 var p1 = new Person(); //此时再定义Person的原型对象 Person.prototype = { constructor : Person, name : "Jon", job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //记住, 实例的指针[[ prototype ]]仅指向原型, 而不指向构造函数 p1.sayJob(); //出错! Uncaught TypeError: p1.sayJob is not a function
原生对象的原型
全部原生的引用类型(Object, Array, String, etc...), 都是使用这种原型模式建立的, 都在其构造函数上定义了方法
经过原生对象的原型, 不只能够取得全部默认方法的引用, 并且也能够定义新方法. 能够像修改自定义对象的原型同样修改原生对象的原型: 便可以随时添加方法(但不推荐) :
console.log(typeof Array.prototype.sort); //function console.log(typeof String.prototype.substr); //function //为原生引用类型String添加方法(不推荐) : String.prototype.startsWith = function(text){ return this.indexOf(text) == 0; } var s1 = "Hi Jon"; console.log(s1.startsWith("Hi")); //true
原型模式的问题 :
省略了为构造函数初始化参数的环节, 致使全部新建的实例都会取得相同的默认值
最大的问题是其共享的本性所致使的, 对于引用类型值的属性来讲问题很是突出 :
function Person(){} Person.prototype = { constructor : Person, name : "Jon", job : "FrontEnd Developer", friends : ["Lucy","Jeniffer"], sayJob : function(){ console.log(this.job); } }; var p1 = new Person(); var p2 = new Person(); p1.friends.push("Quinene"); console.log(p1.friends); //"Lucy", "Jeniffer", "Quinene" console.log(p2.friends); //"Lucy", "Jeniffer", "Quinene" console.log(p1.friends === p2.friends); //true
构造函数模式用于定义实例属性, 原型模式用于定义方法和共享的属性 :
结果每一个实例都有本身的一份实例属性的副本, 但同时又共享着对方法的引用,最大限度的节省了内存:
这种模式还支持向构造函数传参 :
function Person(name, age, job){ //定义实例属性(未来建立实例时不会相同的属性s) this.name = name; this.age = age; this. job = job; this. friends = ["Mark", "Martin"]; } Person.prototype = { //构造函数属性指回Person cosntructor : Person, //定义方法 sayJob : function(){ console.log(this.job); }, //定义共享属性 country : "China" }; //建立实例 var p1 = new Person("Jon", 25, "FrontEnd Developer"); var p2 = new Person("Percy", 26, "DBA"); //为实例p1的friends属性添加值 p1.friends.push("Jeniffer"); console.log(p1.friends); //"Mark", "Martin", "Jeniffer" console.log(p2.friends); //"Mark", "Martin" console.log(p1.friends == p2.friends); //false console.log(p1.sayJob == p2.sayJob); //true console.log(p1.country == p2.country); //true
动态原型模式把全部信息都封装在构造函数中, 而经过在构造函数中初始化原型(仅在必要的状况下), 又保持了同时使用构造函数和原型的优势 :
即 能够经过检查某个应该存在的方法是否有效, 来决定是否须要初始化原型
function Person(name, age, job){ //属性 this.name = name; this.age = age; this.job = job; //方法 if(typeof this.sayJob != "function"){ Person.prototype.sayJob = function(){ console.log(this.job); } } } var p1 = new Person("Jon", 25, "F2E"); p1.sayJob(); //F2E
这里只在sayJob()方法不存在的状况下, 才会将它添加到原型中.
这段代码只会在初次调用构造函数时才会执行.
这里对原型所作的修改, 也会当即在全部实例中获得反映.
if语句检查的能够是初始化以后应该存在的任何属性和方法—— 没必要用一大堆if语句判断每一个属性的方法,只要其中检查一个便可;
这种模式建立的对象能够用instanceof操做符肯定它的类型
基本思路是建立一个函数, 这个函数做用仅仅是封装建立对象的代码, 而后再返回新建立的对象.
function Person(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayJob = function(){ console.log(this.name); }; return o; } var p1 = new Person("Jon", 25, "F2E"); p1.sayJob(); //F2E
Person函数建立了一个新对象o, 并以相应的属性和方法初始化该对象, 而后把它返回.
除了使用new操做符并把使用的包装函数叫作构造函数外, 这个模式跟工厂模式实际上是同样的.
构造函数在不返回值的状况下, 默认会返回新对象的实例, 而经过在构造函数的末尾添加一个return语句, 能够重写调用构造函数时返回的值.
这种模式在特殊的状况下用来为对象建立构造函数.假设咱们想建立一个具备额外方法的特殊数组,
由于不能直接修改Array构造函数, 所以可使用这种模式 :
function SpecialArray(){ //建立一个数组用于接收传入的值 var values = new Array(); //而后使用push方法(用构造函数接收到的全部参数)初始化了数组的值; values.push.apply(values, arguments); //给数组实例添加了一个toPipedString()方法, 该方法返回以短横线分割的数组值; values.toPipedString = function(){ return this.join("-"); }; //将数组以函数值的形式返回. return values; } var colorsArr = new SpecialArray("red", "blue", "purple"); console.log(colorsArr.toPipedString()); //red-blue-purple //关于该模式 : 首先, 返回的对象与构造函数或者构造函数的原型属性之间没有关系;也就是说, 构造函数返回的对象与在构造函数外部建立的对象没有什么不一样.为此不能依赖instance操做符来肯定对象类型. console.log(colorsArr instanceof SpecialArray); //false
稳妥对象 : 没有公共属性, 其方法也不引用this的对象.
适合在安全的环境中(禁止使用this和new), 或者在防止数据被其余应用程序改动时使用
稳妥构造函数遵循与寄生构造函数相似的模式, 但有两点不一样 :
一是新建立对象的实力方法不引用this,
二是不适用new操做符调用构造函数 :
function Person(name, age, job){ //建立要返回的对象 var o = new Object(); //能够在这里定义私有变量和方法 //添加方法 o.sayJob = function(){ console.log(job); } //返回对象 return o; } /*这种方式建立的对象中, 除了使用sayJob()方法外, 没有其余办法访问job的值*/ //使用稳妥的Person构造函数 var p1 = new Person("Jon", 25, "FrontEnd Developer"); p1.sayJob(); //FrontEnd Developer console.log(p1.job); //尝试直接访问job属性会返回undefined
许多OO语言都支持两种继承方式 :
接口继承 : 只继承方法签名
实现继承 : 继承实际的方法
因为函数没有签名, 在ES中没法实现接口继承.ES只支持实现继承, 并且其 实现继承 主要是依靠原型链实现的.
方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成。
方法签名应该以下所示,相应的可变参数分别使用String和Exception声明:
Log.log(String message, Exception e, Object... objects) {...}
利用原型让一个引用类型继承另外一个引用类型的属性和方法.
简单回顾下构造函数, 原型, 实例的关系 :
每一个构造函数都有一个原型对象( prototype ), 原型对象都包含一个指向构造函数的指针( constructor ), 而每一个实例都包含一个指向原型对象的内部指针( [[ prototype ]], __proto__ )
那么,假如咱们让原型对象(prototype)等于另外一个类型的实例, 那么此时的原型对象将包含一个指向另外一个原型的指针.
相应地, 另外一个原型中也包含着一个指向另外一个构造函数的指针.
假如另外一个原型又是另外一个原型的实例, 那么上述关系依然成立, 如此层层递进, 就构成了实力与原型的链条, 这就是所谓的原型链的概念.
实现原型链的基本模式 :
/*定义两个类型, SuperType和SubType*/ function SuperType(){ //SuperType本身的属性 this.property = true; } SuperType.prototype.getSuperValue = function(){ //SuperType本身的方法 return this.property; } function SubType(){ //SubType本身的属性 this.subproperty = false; } /*SupType经过建立SuperType()的实例继承了SuperType, 并赋值给SubType.prototype, 即SubType的原型对象实现的本质是重写原型对象, 代之以一个新类型的实例. 换句话说, 原来存在于SuperType的实例中的全部属性和方法, 如今也存在于SubType.prototype中了 */ SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ //添加SubType本身的方法, 这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法 return this.subproperty; } //建立一个新实例 var instance = new SubType(); console.log(instance.getSuperValue()); //true //测试是否为Object, SuperType, SubType的实例 console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
关系如图所示 :
最终结果 :
instance
实例指向SubType
的原型, SubType
的原型又指向SuperType
的原型.
getSuperValue()
方法仍然还在SuperType.prototype
中, 但property
则位于SubType.prototype
中.
这是由于, property是一个实例属性,而getSuperType()则是一个原型方法
既然SubType.prototype
如今是SuperType
的实例, 那么prototype
固然就位于该实例中了.
要注意,实例的 instance.constructor
如今指向的是SuperType
, 这是由于SubType
的原型如今指向了另外一个对象—— SuperType
的原型.
而这个原型对象的constructor
属性指向的是SuperType
.
全部引用类型默认都继承了Object, 而这个继承也是经过原型链实现的.
要记住, 全部函数的默认原型都是Object的实例, 所以默认原型都会包含一个内部指针指向Object.prototype.
这也正是全部自定义类型都会继承toString(), valueOf()的根本缘由.
因此, 上面例子展现的原型链应该还包含另外一个继承层次 : (完整的原型链以下)
使用instanceof
操做符, 测试实例和原型链中出现过的构造函数, 若是存在就会返回true
使用isPrototypeOf()
方法, 只要是原型链中出现过的原型, 均可以说是该原型链所派生的实例的原型,所以该方法会返回true
//上面第一段代码的最后片断 : console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
给原型添加方法的代码必定要放在替换原型的语句以后 :
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //从SuperType继承 SubType.prototype = new SuperType(); //SubType本身的新方法 SubType.prototype.getSubValue = function(){ return this.subproperty; } //SubType继承的父类方法getSuperValue()被重写, 但只会重写SubType自身的getSuperValue(), 不会影响上一级父类原来的方法, 即若是调用的是SuperType的getSuperValue()方法的话仍是会返回原来的true. SubType.prototype.getSuperValue = function(){ return false; } //建立实例 var ins1 = new SubType(); var ins2 = new SuperType(); console.log(ins1.getSuperValue()); //false, 重写的方法 console.log(ins2.getSuperValue()); //true, SubType重写getSuperValue()方法并不会影响父类原有的方法
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //从SuperType继承 SubType.prototype = new SuperType(); /*刚刚把SuperType的实例赋值给SubType的原型⬆️, 又使用对象字面量⬇️把原型替换 SubType.prototype = {...}, 因此如今SubType的原型包含的是 一个属于Object的实例而不是SuperType的, 原先的原型链已经被切断, SubType与SuperType已经没有任何关系了*/ //使用对象字面量把 原型替换 SubType.prototype = { getSubValue : function(){ return this.subproperty; }, someOtherMethod : function(){ return false; } } var ins1 = new SubType(); console.log(ins1.getSuperValue()); //Uncaught TypeError: ins1.getSuperValue is not a function
最主要的问题来自包含引用类型值的原型.
以前说过, 包含引用类型值的原型属性会被全部实例共享.
而这也是为何要在构造函数中, 而不是原型对象中定义属性的缘由.
第二个问题是, 在建立子类型的实例时, 不能向超类型的构造函数传递参数
在经过原型来实现继承时, 原型实际上会变成另外一个类型是实例( SubType.prototype = new SuperType(); ), 因而, 原先的实例属性也就瓜熟蒂落的变成了如今的原型属性了.
function SuperType(){ this.colors = ["red", "green", "blue"]; } function SubType(){} SubType.prototype = new SuperType(); var ins1 = new SubType(); console.log(ins1.colors); // "red", "green", "blue" //在ins1添加colors属性的属性值 ins1.colors.push("purple"); console.log(ins1.colors); //"red", "green", "blue", "purple" var ins2 = new SubType(); //ins1中添加到colors中的属性值直接被添加到了SubType()的原型属性里面, 致使后来新增的实例也继承了这些属性 console.log(ins2.colors); //"red", "green", "blue", "purple"]
思路 : 在子类型构造函数的内部调用超类型的构造函数.
函数只不过是在特定环境中执行代码的对象, 所以能够经过apply()
和call()
方法也能够在(未来)新建立的对象上执行构造函数
function SuperType(){ this.colors = ["green", "blue", "purple"]; } function SubType(){ //继承自SuperType //当SubType(){...}被实例化后, SuperType()函数中定义的全部对象初始化代码就会被执行 SuperType.call(this); } var ins1 = new SubType(); ins1.colors.push("red"); console.log(ins1.colors); //"green", "blue", "purple", "red" var ins2 = new SubType(); console.log(ins2.colors); //"green", "blue", "purple"
//相比原型链, 借用构造函数还有一个很大的优点, 就是子类型的构造函数能够向超类型的构造函数传递参数 function SuperType(name){ //父类构造函数接受一个name函数, 并赋值给一个属性 this.name = name; } function SubType(){ /*在SubType()构造函数中调用SuperType()构造函数时, 其实是为SubType的实例设置了name属性*/ SuperType.call(this, "Jon"); /*为了确保SuperType构造函数不会重写子类型的属性, 能够在调用父类构造函数后,再添加应该在子类型中定义的属性*/ this.age = 25; } var ins1 = new SubType(); console.log(ins1.name); //Jon console.log(ins1.age); //25
若是仅仅是借用构造函数, 那么也没法避免构造函数模式存在的问题—— 方法都在构造函数内部定义, 那么函数复用就无从谈起了.
并且在超类型的原型中定义的方法, 对子类型而言也是不可见的, 结果全部类型都只能使用构造函数模式.
因此这种方式也是不多单独使用的
指的是将 原型链 与 借用构造函数 的技术组合到一块, 从而发挥两者之长的一种继承模式.
思路是, 使用 原型链 实现 对原型属性和方法的继承 , 而经过 借用构造函数 来实现对 实例属性的继承
这样, 既经过在原型上定义方法实现了函数复用, 又可以保证每一个实例都有本身的属性. 因此这成为JavaScript中经常使用的继承方式
function SuperType(name){ //父类定义两个属性name和colors this.name = name; this.colors = ["blue", "red", "yellow"]; } //父类定义原型方法sayName SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ //SubType构造函数在调用SuperType构造函数时传入了name参数 SuperType.call(this, name); //而后定义本身的属性age this.age = age; } //将SuperType的实例赋值给SubType的原型 SubType.prototype = new SuperType(); //name, colors[], sayName() SubType.prototype.constructor = SubType; //构造函数指回本身 //在该新原型上定义了方法sayAge() SubType.prototype.sayAge = function(){ console.log(this.age); } //两个不一样的SubType实例既分别拥有本身的属性————包括colors属性, 又可使用相同的方法了 var ins1 = new SubType("Jon", 25); ins1.colors.push("purple"); console.log(ins1.colors); //"blue", "red", "yellow", "purple" ins1.sayName(); //Jon ins1.sayAge(); //25 var ins2 = new SubType("Mark", 24); console.log(ins2.colors); //"blue", "red", "yellow" ins2.sayName(); //Mark ins2.sayAge(); //24
借助原型能够基于已有的对象建立新对象, 同时还没必要所以建立自定义类型.
/* 在object()函数内部, 先建立了一个临时性的构造函数F(){}, 而后将传入的对象o做为这个构造函数F(){}的原型, 最后返回了这个临时类型的新实例. 从本质上讲, object()对传入其中的对象o执行了一次浅复制 */ /* 这种继承方式要求你必须有一个对象能够做为另外一个对象的基础, 把它传给object()函数,而后再根据具体需求对获得的对象加以修改便可. */ function object(o){ function F(){} F.prototype = o; return new F; } /* 这个例子中, 能够做为另外一个对象的基础是person对象 */ var person = { name : "Jon", colorsLike : ["black", "white"] } /* 把它(person对象)传入到object()函数中, 而后该函数就会返回一个新对象( anotherPerson1 和 anotherPerson2 ), 这两个新对象把person做为原型, 全部它们的原型中就包含一个基本类型值属性和 一个引用类型值属性,这意味着person.colorsLike不只于person全部,同时也会被 anotherPerson1, anotherPerson2共享, 实际上, 就至关于又建立了person对象的两个副本 */ var anotherPerson1 = object(person); //anotherPerson1如今有了person的全部属性(这里是name和colorsLike[]) console.log(anotherPerson1.name); //person原有的name属性值, 输出Jon console.log(anotherPerson1.colorsLike); //person原有的colorsLike[]数组, 输出["black", "white"] anotherPerson1.name = "Percy"; //修改anotherPerson1的name属性为本身的值 anotherPerson1.colorsLike.push("purple"); //添加anotherPerson1本身喜欢的颜色 console.log(anotherPerson1.name); //Percy console.log(anotherPerson1.colorsLike); //["black", "white", "purple"] console.log(person.name); //Jon console.log(person.colorsLike); //person的colorsLike数组值已经被anotherPerson1添加的属性影响, 此时也输出了["black", "white", "purple"] var anotherPerson2 = object(person); console.log(anotherPerson2.name); //Jon console.log(anotherPerson2.colorsLike); //["black", "white", "purple"] anotherPerson2.colorsLike.push("red"); //再push一个 console.log(anotherPerson2.colorsLike); //["black", "white", "purple", "red"] console.log(person.colorsLike); //再度被anotherPerson2新增的值影响, 输出["black", "white", "purple", "red"]
ES5新增了一个方法Object.create()
规范了原型式继承, 该方法接收两个参数, 一个用做新对象的原型的对象和一个(可选)一个为新对象定义额外属性的对象
浏览器支持, IE9+和各现代浏览器
仍是直接看例子比较直观 :
传入一个参数的时候, 这个方法跟上面object()方法的行为相同 :
var person = { name : "Jon", colorsLike : ["black", "white"] }; console.log(person.colorsLike); // ["black", "white"] //传入一个参数的时候, 这个方法跟上面object()方法的行为相同 var anotherPerson = Object.create(person); anotherPerson.name = "Percy"; anotherPerson.colorsLike.push("purple"); console.log(anotherPerson.name); //Percy console.log(anotherPerson.colorsLike); //["black", "white", "purple"] console.log(person.name); //Jon console.log(person.colorsLike); //["black", "white", "purple"]
传入两个参数的时候, 第二个参数与Object.defineProperties()方法的第二个参数格式相同 : 每一个属性都是经过本身的描述符定义的, 以这种方式指定任何属性都会覆盖原型对象上的同名属性
var person = { name : "Jon", colorsLike : ["black", "white"] }; console.log(person.colorsLike); // ["black", "white"] //传入两个参数 var anotherPerson = Object.create(person, { name : { value : "Martin" } }); console.log(anotherPerson.name); //Martin
与原型式继承紧密相关的思路, 与寄生构造函数和工厂模式相似, 即建立一个仅用于封装继承过程的函数, 该函数在内部以某种形式来加强对象, 最后再像真的是它作了全部工做同样返回对象
function object(o){ function F(){} F.prototype = o; return new F; } function createAnother(original){ var clone = object(original); //经过调用函数建立一个新对象 clone.sayHi = function(){ //以某种方式加强这个对象(添加自身方法或者属性等) console.log("Good Day!"); }; return clone; //返回该对象 } var person = { name : "Jon", friends : ["Martin", "Jeniffer"] }; /* 这个实例中的代码 基于person 返回了一个新对象————anotherPerson 该对象不只具备person全部属性和方法, 并且还有本身的sayHi()方法 */ /* 在主要考虑对象而不是自定义类型和构造函数的状况下, 寄生式继承也是一种有用的方式, 前面示范继承模式使用的object()函数并非必须的, 任何可以返回新对象的函数都适用于此模式 */ var anotherPerson = createAnother(person); anotherPerson.sayHi(); //Good Day!
前面说过, 组合继承是JS中最经常使用的继承模式, 不过它也有本身的不足
组合继承 最大的问题是, 不管在什么状况下, 都会调用两次父类型构造函数, 一次是在建立子类型原型的时候, 一次是在子类型构造函数内部 :
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"] } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ SuperType.call(this, name); //第二次调用SuperType() this.age = age; } /* 第一次 调用SuperType构造函数时, SubType.prototype会获得两个属性, name和colors[], 它们都是SuperType的实例属性, 只不过如今位于SubType的原型中; 当调用SubType构造函数时, 又会再一次调用一次SuperType构造函数, 这一次又在新对象上建立了实例属性name和colors[], 因而, 这两个属性就遮蔽了原型中的两个同名属性 */ SubType.prototype = new SuperType(); //第一次调用SuperType() SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }
以下图 :
寄生组合式继承, 即经过借用构造函数来继承属性, 经过原型链的混成模式来继承方法.
思路是, 没必要为了指定子类型的原型而调用构造超类型的构造函数, 咱们所须要的无非就是超类型的一个副本而已 ⬇️
本质上, 就是使用寄生式继承来继承超类型的原型, 而后再将结果指定给子类型的原型.
基本模式以下所示. ⬇️
function object(o){ function F(){} F.prototype = o; return new F(); } /* 寄生组合式继承的最简单形式, 这个函数接收两个参数, 子类型构造函数和超类型构造函数; 在函数内部,第一步是建立超类型原型的一个副本, 第二步是为建立的的副本添加constructor属性, 从而弥补因重写而失去默认的constructor属性; 最后一步, 将新建立的对象(即副本)赋值给子类型的原型,这样咱们就能够调用inheritPrototype()函数的语句,去替换前面例子中未知类型原型赋值的语句了(41行) */ function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //建立对象 prototype.constructor = subType; //加强对象 subType.prototype = prototype; //指定对象 } function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); //调用inheritPrototype()函数 SubType.prototype.sayAge = function(){ console.log(this.age); }; var instance1 = new SubType("Jon", 25); instance1.colors.push("black"); console.log(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Jon" instance1.sayAge(); //25 var instance2 = new SubType("Mark", 24); console.log(instance2.colors); //"red,blue,green" instance2.sayName(); //"Mark" instance2.sayAge(); //24
第一种是函数声明 :
function Person(name){ this.name = name; console.log("name is " + this.name); } Person("Jon"); //name is Jon //函数声明支持函数声明提高, 即执行代码前会先读取函数声明, 那么函数声明能够放在调用它的代码以后而不出错 : Person("Jon"); //works ! 输出name is Jon function Person(name){ this.name = name; console.log("name is " + this.name); }
第二种是函数表达式 :
var Person = function(name){ this.name = name; console.log(this.name); } Person("Jon"); //Jon //函数表达式不支持函数声明提高 Person("Jon"); //Uncaught TypeError: Person is not a function var Person = function(name){ this.name = name; console.log(this.name); }
要在使用条件语句后面执行函数的话, 条件语句内的函数必须使用函数表达式的方式定义, 若是使用函数声明方式定义, 会在不一样的浏览器致使不一样问题的发生 :
//条件语句内的函数定义必须使用函数表达式 var b = true; if(b){ sayColors = function(){ console.log(this.color); }; }else{ console.log("error!"); }
function createComparisonFunction(propertyName){ //这里返回的就是匿名函数, 它能赋值给一个变量, 或者以其余的方式调用 return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; }else if(value1 > value2){ return 1; }else{ return 0; } }; }
递归函数是一个函数经过名字调用自身的状况下构成的
//递归阶乘函数 function factorial(num){ if(num <= 1){ return 1; }else{ return num * factorial(num - 1); } } //注意以下调用会产生错误 var anotherFactorial = factorial; //把factorial()函数保存在一个变量中 factorial = null; //把factorial函数设置为null console.log(anotherFactorial(3)); //Uncaught TypeError: factorial is not a function
//使用arguments.callee解决上面的问题 //arguments.callee是一个指向当前正在执行的函数的指针, 所以能够用它来实现对函数的递归调用 //严格模式下不容许使用arguments.callee function factorial(num){ if (num <= 1) { return -1; }else{ //arguments.callee代替了函数名factorial return num * arguments.callee(num - 1); } }
//解决严格模式下不容许使用arguments.callee的问题 //使用命名函数表达式来达成相同的结果 var factorial = (function f(num){ //建立一个名为f()的命名函数表达式, 赋值给factorial if(num <= 1){ return 1; }else{ return num * f(num - 1); } });
注意匿名函数与闭包不要混淆.
闭包指的是有权访问 另外一个函数做用域中的变量 的函数
建立闭包经常使用的方式, 就是在一个函数内部建立另外一个函数 :
function createComparisonFunction(propertyName){ return function(object1, object2){ //value1和value2访问了外部函数的变量propertyName, 即便该内部函数被返回或被其余地方调用, 也不影响它访问外部函数的propertyName变量(由于该外部变量在本内部函数的做用域内) var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; }else if(value1 > value2){ return 1; }else{ return 0; } }; }
理解 :
//定义compare函数 function compare(value1, value2){ if(value1 < value2){ return -1; }else if(value1 > value2){ return 1; }else{ return 0; } } //在全局做用域中调用函数, 从做用域链的优先级来分的话, 外部函数的活动对象始终处于第二位, 外部函数的外部函数的活动对象处于第三位 ...(以此类推), 直到做为做用域链终点的全局执行环境 var result = compare(5, 8); //在调用compare()函数时, 会建立一个包含arguments, value1, value2的活动对象(在做用域链的优先级处于第一位), 全局执行环境的变量对象(包含result和compare)在compare()执行环境的做用域链优先级处于第二位
做用域链优先级图示 :
后台的每个执行环境都有一个表示变量的对象 — — 变量对象
全局环境的变量对象始终存在, 而像compare()函数这样的局部环境的变量对象, 则只在函数执行的过程当中存在.
建立compare()函数时, 会建立一个预先包含全局变量对象的做用域链, 该做用域链会被保存在内部的[[ Scope ]]
属性中
调用compare()函数时, 会为函数建立一个执行环境
而后经过复制函数的[[ Scope ]]
属性中的对象构建起执行环境的做用域链
未完待续...
TODO
TODO