摘要:对象对象创建继承早期创建对象的方式对象字面量创建方式亦可换成因为指向当前对象的两种属性数据属性和访问器属性数据属性数据属性包含个数据值的位置这个位置可以读取和写入值名称描述表示能否通过删除属性而重新定义属性能否修改属性的特性或者能否把属性修
JavaScript OOP, 对象, 对象创建, 继承
//早期创建对象的方式 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的两种属性, 数据属性和访问器属性
数据属性数据属性包含1个数据值的位置, 这个位置可以读取和写入值
名称 | 描述 |
---|---|
[[ Configurable ]] | 表示能否通过delete删除属性而重新定义属性, 能否修改属性的特性, 或者能否把属性修改为访问器属性, 默认值true |
[[ Enumerable ]] | 表示能否通过for-in循环返回属性, 默认值true |
[[ Writable ]] | 表示能否修改属性的值, 默认值true |
[[ Value ]] | 包含这个属性的数据值. 读取属性值的时候, 从这个位置读; 写入属性值的时候, 把新值保存在这个位置, 默认值undefined |
var person = { name : Jon, //value值变成Jon };
接收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函数,该函数负责返回有效的值;
写入访问器属性时, 会调用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()
可以通过描述符一次过定义多个属性
接收两个参数 :
要添加和修改其属性的对象
第二个对象的属性与第一个对象中要添加或修改的属性一一对应.
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
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/86308.html
摘要:第一部分请点击快速掌握面试基础知识一关键字如果使用关键字来调用函数式很特别的形式。该对象默认包含了指向原构造函数的属性。接下来通过例子来帮助理解属性包含了构造函数以及构造函数中在上定义的属性。也就是说,的回调函数后执行。 译者按: 总结了大量JavaScript基本知识点,很有用! 原文: The Definitive JavaScript Handbook for your next...
摘要:特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 本以为自己收藏的站点多,可以很快搞定,没想到一入汇总深似海。还有很多不足&遗漏的地方,欢迎补充。有错误的地方,还请斧正... 托管: welcome to git,欢迎交流,感谢star 有好友反应和斧正,会及时更新,平时业务工作时也会不定期更...
摘要:现在回过头总结,才又进一步的揭开了闭包的一层后台管理系统解决方案前端掘金基于系列的后台管理系统解决方案。什么是继承大多数人使用继承不外乎是为了获得基于的单页应用项目模板前端掘金小贴士本项目已升级至。 关于js、jq零碎知识点 - 掘金写在前面: 本文都是我目前学到的一些比较零碎的知识点,也是相对偏一点的知识,这是第二篇。前后可能没有太大的相关性,需要的朋友可以过来参考下,喜欢的可以点个...
阅读 609·2023-04-26 02:03
阅读 959·2021-11-23 09:51
阅读 1054·2021-10-14 09:42
阅读 1672·2021-09-13 10:23
阅读 880·2021-08-27 13:12
阅读 803·2019-08-30 11:21
阅读 967·2019-08-30 11:14
阅读 982·2019-08-30 11:09