js是一个基于对象的语言,因此本文研究一下js对象和类实现的过程和原理。javascript
下面是一个对象的各个部分:java
var person = { name: "Lily", age: 10, work: function(){ console.log("Lily is working..."); } }; person.gender = "F"; //能够动态添加属性 Object.defineProperty(person, "salary", { //添加属性 value: 10000, writable: true, //是否可写,默认false enumerable: true, //是否可枚举,默认false configuration: true //是否可配置,默认false; }); Object.defineProperties(person, { //添加多个属性 "father": { value: Bob, enumerable: true }, "mather": { value: Jelly, enumerable: true } }); delete person.age; // 删除属性 Object.getOwnPropertyDescriptor(person, "father"); //{ value:10000,writable:true,enumerable:true,configuration:true}
是否可写指得是其值是否可修改;
是否可枚举指的是其值是否能够被for...in...遍历到;
是否可配置指的是其可写性,可枚举性,可配置性是否可修改,而且决定该属性能否被删除。c++
这是一个普通的对象和常见操做,很少说,下面是一个具备get/set的对象:编程
var person = { _age: 11, get age(){ return this._age; }, set age(val){ this._age = val; } }; //以下方法访问: console.log(o.age); //读 o.age = 30; //写 console.log(o.age);
上文,咱们只提到了对象属性的4个性质,对象本身其实也有3个性质:segmentfault
可不可扩展是指一个对象可不能够添加新的属性;Object.preventExtensions 可让这个对象变的不可扩展。尝试给一个不可扩展对象添加新属性的操做将会失败,但不会有任何提示,(严格模式下会抛出TypeError异常)。Object.preventExtensions只能阻止一个对象不能再添加新的自身属性,仍然能够为该对象的原型添加属性,但__proto__属性的值也不能修改。浏览器
var person = { name: "Lily", age: 10 }; //新建立的对象默认是可扩展的 console.log(Object.isExtensible(person)); //true person.salary = 10000; console.log(person.salary) //10000 Object.preventExtensions(person);//将其变为不可扩展对象 console.log(Object.isExtensible(person)); //false person.height = 180; //失败,不抛出错误 console.log(person.height); //undefined person.__proto__.height = 180; //在其原型链上添加属性 console.log(person.height); //180 delete person.age; //能够删除已有属性 console.log(person.age); //undefined person.__proto__ = function a(){}; //报错TypeError: #<Object> is not extensible(…) function fun(){ 'use strict' person.height = 180; //报错TypeError: #<Object> is not extensible(…) } fun(); Object.defineProperty("height", { value: 180 }); //因为函数内部采用严格模式,因此报错TypeError: #<Object> is not extensible(…)
这里若是不理解__proto__
没关系,下文会重点解释这个属性网络
若是咱们想让一个对象即不可扩展,又让它的全部属性不可配置,一个个修改属性的configurable太不现实了,咱们把这样的对象叫作密封的(Sealed)。用Object.isSealed()判断一个对象是否密封的,用Object.seal()密封一个对象。 其特性包括不可扩展对象和不可配置属性的相关特性。数据结构
var person = { name: "Lily", age: 10 }; //新建立的对象默认是不密封的 console.log(Object.isSeal(person)); //false Object.seal(person);//将其变为密封对象 console.log(Object.isSeal(person)); //true delete person.age; //没法删除已有属性,失败,不报错。但严格模式会报错 console.log(person.age); //undefined person.__proto__ = function a(){}; //报错TypeError: #<Object> is not extensible(...)
此时,这个对象属性可能仍是可写的,若是咱们想让一个对象的属性既不可写也不可配置,同时让该对象不可扩展,那么就须要冻结这个对象。用Object.freeze()冻结对象,用isFrozen()判断对象是否被冻结。因为相比上一个例子,仅仅是现有的变得不可写了,这里就不举太多例子了。
不过值得注意的是,对于具备setter的属性同样不可写。app
var person = { name: "Lily", _age: 10, get age(){ return this._age; }, set age(val){ this._age = val; } }; //新建立的对象默认不是冻结的 console.log(Object.isFrozen(person)); //false Object.freeze(person);//将其变为不可扩展对象 console.log(Object.isExtensible(person)); //false console.log(Object.isSealed(person)); //true console.log(Object.isFrozen(person)); //true console.log(person.name); //"Lily" person.name = "Bob"; //失败,但不报错,但严格模式会报错。 console.log(person.name); //"Lily" console.log(person.age); //10 person.age = 30; console.log(person.age); //10
深冻结和浅冻结的主要差别出如今可扩展性上,因此你也能够理解为深可扩展和浅可扩展。咱们看一下如下代码:函数
var person = { addr: {} } Object.freeze(person); person.addr.province = "Guangzhou"; //浅冻结:对象的属性对象能够继续扩展 console.log(person.addr.province); //"Guangzhou"
为了实现深冻结,咱们写一个函数:
var person = { name: "nihao", addr: {}, family:{ slibing:{}, parents:{} } } deepFreeze(person); person.addr.province = "Guangzhou"; //深冻结:对象的属性对象没法继续扩展 console.log(person.addr.province); //undefined person.family.parents.father = "Bob"; //深冻结:对象的属性对象没法继续扩展 console.log(person.family.parents.father); //undefined function deepFreeze(obj){ Object.freeze(obj); for(key in obj){ if(!obj.hasOwnProperty(key)) continue; if(obj[key] !== Object(obj[key])) continue; deepFreeze(obj[key]); //递归调用 } }
注意,这里递归没有判断链表是否成环,判断有环链表是数据结构的知识,可使用一组快慢指针实现,这里不赘述。所以在如下状况会有一个bug:
function Person(pname, sname){ this.name = pname || ""; this.spouse = sname || {}; } var p1 = new Person("Lily"); var p2 = new Person("Bob", p1); p1.spouse = p2; deepFreeze(p1); //会陷入无休止的递归。实际家庭成员关系更复杂,就更糟糕了。RangeError: Maximum call stack size exceeded(…)
当咱们想建立不少我的的时候,就不会像上面这样一个一个写了。那咱们就造一个工厂,用来生产人(感受有点恐怖):
function CreatePerson(pname, page){ return { name: pname, age: page }; } p1 = CreatePerson("Lily", 21); p2 = CreatePerson("Bob", 12); console.log(p1); //Object {name: "Lily", age: 21} console.log(p2); //Object {name: "Bob", age: 12}
可是这样写并不符合传统的编程思路。所以咱们须要一个构造函数(constructor, 也有书译为构造器)
关于构造函数和普通函数的区别能够看javascript中this详解中”构造函数中的this"一节。
下面定义一个构造函数:
function Person(pname, page){ this.name = pname; this.age = page; this.work = function(){ console.log(this.name + " is working..."); }; } var p1 = new Person("Lily",23); var p2 = new Person("Lucy", 21); console.log(p1); p1.work(); console.log(p2); p2.work();
不过这样写这个函数,每一个对象都会包括一部分,太浪费内存。因此咱们会把公共的部分放在prototype中:
function Person(pname, page){ this.name = pname; this.age = page; } Person.prototype.work = function(){ console.log(this.name + " is working..."); }; var p1 = new Person("Lily",23); var p2 = new Person("Lucy", 21); console.log(p1); p1.work(); console.log(p2); p2.work();
经过上面的输出,咱们看到,每一个对象(p1,p2)都包含了一个__proto__
属性,这个是一个非标准属性(ES6已经把它标准化了),不过IE中没有这个属性。
在学习原型链以前咱们必定要区分清楚:prototype是构造函数的属性,而__proto__
是对象的属性。固然咱们依然用代码说话:
再来一段代码:
function Person(pname, page){ this.name = pname; this.age = page; } Person.prototype.work = function(){ console.log(this.name + " is working..."); }; var p = new Person("Lily",23); console.log(p.constructor); //function Person(){...} console.log(p.__proto__); //Object console.log(Person.prototype); //Object console.log(Person.prototype.constructor); //function Person(){...} console.log(Person.__proto__); console.log(Person.constructor); console.log(Person.__proto__); //空函数function(){} console.log(Person.constructor); //function Function(){...}
说到这里,就有必要学习一下原型链了。
js没有类的概念,这样就不会有继承派生和多态,可是实际编程中咱们须要这样的结构,因而js在发展过程当中,就从一个没有类的语言模拟出来类的效果,这里靠的就是prototype。
一个构造函数的prototype永远指向他的父对象,这样这个构造函数new出来的对象就能够访问其父对象的成员,实现了继承。
若是他的父对象的prototype又指向一个父对象的父对象,这样一层层就构成了原型链。以下(用浏览器内置对象模型举例):
console.log(HTMLDocument); console.log(HTMLDocument.prototype); //HTMLDocument对象 console.log(HTMLDocument.prototype.constructor.prototype); console.log(HTMLDocument.prototype.constructor.prototype.constructor.prototype); console.log(HTMLDocument.prototype.constructor.prototype.constructor.prototype.constructor.prototype); console.log(HTMLDocument.prototype.constructor.prototype.constructor.prototype.constructor.prototype.constructor.prototype); /*......*/
若是你以为这里应该有一张图,那就看看这个完整的对象关系图(基于DOM),下文的相关例子也基于这个图:
注意:原型链是有穷的,他总会指向Object,而后是null结束
那么__proto__
是什么?一言以蔽之:对象的__proto__
属性指向该对象构造函数的原型。以下:
function Person(pname, page){ this.name = pname; this.age = page; this.work = function(){ console.log(this.name + " is working..."); }; } var o = new Person("Lily",23); o.__proto__ === Person.prototype //true
上面图中发现,对象还有一个constructor属性,这个属性也很重要,新建立对象的constructor指向默认对象的构造函数自己,不过现实没有这么简单。例如:
function Person(){ } var p1 = new Person(); console.log(p1.constructor); //function Person(){...} function Children(){ } Children.prototype = p1;//这一行和下一行联立使用,不能忽略下一行 Children.prototype.constructor = Children; //修正constructor,这个不能省略 console.log(Person.prototype.constructor); //function Person(){...} console.log(p1.constructor); //function Child(){...}
当咱们创建了一个继承关系后,会使新的构造函数的prototype.constructor指向改构造函数本身,像上面第9行同样。从第11行也能够看出,系统自己也是这样作的。这样就构成了下面这个图的关系,此时父对象的constructor指向子构造函数:
注: 图片来自网络
从上面的这些例子咱们不难发现,函数也是一个对象。所以构造函数也有了constructor和__proto__属性。不过这里会比较简单:函数的constructor都是Function(){...};函数的__proto__
都是个空函数
其实在js中除了基本类型(null, undefined, String, Number, Boolean, Symbol)之外,都是对象。可能你想反驳我:“js中一切都是对象”。咱们看如下几个例子:
//以数字类型为例 var a = 1; //基本类型 console.log(a); //1 console.log(typeof a); //number var b = new Number(1); //对象类型的数字 console.log(b); //Number {[[PrimitiveValue]]: 1} console.log(typeof b); //object
首先,js中基本类型中除了null和undefined之外的类型,都具备对象形式。但对象形式不等于基本类型。从上面的输出结果来看,var a = 1;和var a = new Number(1);彻底不是一回事。你或许会反驳我:"a有方法呀,基本类型怎么会有方法!!",咱们再看下一个例子:
var a = 1; console.log(a.toFixed(2)); //1.00 var b = new Number(1); console.log(b + 2); //3
上面的例子看似基本类型a有了方法,对象又能够参与运算。实际上这是隐式类型转换的结果,上面第二行,浏览器自动调用了new Number()把a转换成了对象,而第四行利用ValueOf()方法把对象转换成了数字。
既然函数也是个对象,那么咱们不只能够用构造函数new一个对象出来,也能够为它定义私有方法(变量)和静态方法
function Person(pname){ var age = 10; //私有变量,外面访问不到 function getAge(){ //私有方法,外面访问不到 console.log(age); } this.name = pname; this.getInfo = function(){ //公有方法,也能够定义在prototype中 console.log(this.name); getAge.call(this); //注意这里的做用域和调用方式 }; }; Person.speak = function(){console.log("I am a person");}; //静态方法 var p = new Person("Bob"); p.getInfo(); //Bob 10 Person.speak(); //"I am a person"
固然实现简单的对象继承不用这么复杂,可使用Object.create(obj);返回一个继承与obj的对象。对与Object.create()方法须要考虑一下几种状况:
var o = {}; var r1 = Object.create(o); //建立一个r1继承于o var r2 = Object.create(null); //建立一个r2继承于null var r3 = Object.create(Object); //建立一个r3继承于Object console.log(r1); //是一个继承自o的对象 console.log(r2); //是一个空对象,没有__proto__属性 console.log(r3); //是一个函数
有了先前的知识,咱们能够写出来一个函数实现Object.create()
function inherit(o){ //if(Object.create) return Object.create(o); if(o !== Object(o) && o !== null) throw TypeError("Object prototype may only be an Object or null"); function newObj(){}; newObj.prototype = o || {}; var result = new newObj(); if(o === null) result.__proto__ = null; return result; } var obj = {}; console.log(Object.create(obj)); console.log(inherit(obj)); console.log(Object.create(null)); console.log(inherit(null)); console.log(Object.create(Object)); console.log(inherit(Object));
看了这么多,怎么写继承比较合理,咱们实现2个构造函数,让Coder继承Person。比较如下3种方法:
function Person(pname){ this.name = pname; } function Coder(){} //方法一:共享原型 Coder.prototype = Person.prototype; //方法二:实例继承 Coder.prototype = new Person("Lily"); Coder.prototype.constructor = Coder; //方法三:本质上仍是实例继承 Coder.prototype = Object.create(Person.prototype);
固然还有其余的继承方法:
//方法4:构造继承 function Person(pname){ this.name = pname; } function Coder(pname){ Person.apply(this, argument); } //方法5:复制继承 function Person(pname){ this.name = pname; this.work = function() {...}; } var coder = deepCopy(new Person()); //拷贝 coder.code = function(){...}; //扩展新方法 coder.language = "javascript"; //扩展新属性 coder.work = function() {...}; //重构方法 //下面是深拷贝函数 function deepCopy(obj){ var obj = obj || {}; var newObj = {}; deeply(obj, newObj); function deeply(oldOne, newOne){ for(var prop in oldOne){ if(!oldOne.hasOwnProperty(prop)) continue; if(typeof oldOne[prop] === "object" && oldOne[prop] !== null){ newOne[prop] = oldOne[prop].constructor === Array ? [] : {}; deeply(oldOne[prop], newOne[prop]); } else newOne[prop] = oldOne[prop]; } } return newObj; }
既然方法这么多,咱们该如和选择,一张表解释其中的区别
--- | 共享原型 | 实例继承 | 构造继承 | 复制继承 |
---|---|---|---|---|
原型属性 | 继承 | 继承 | 不继承 | 继承 |
本地成员 | 不继承 | 继承 | 继承 | 继承 |
子类影响父类 | Y | N | N | N |
执行效率 | 高 | 高 | 高 | 低 |
多继承 | N | N | Y | Y |
obj instanceof Parent | true | false | false | false |
子类的修改会影响父类是绝对不行的,因此共享原型是不能用的。在考虑到使用方便,只要不涉及多继承就用实例继承,多继承中构造继承也好于复制继承。
instanceof用来判断对象是否某个构造函数的实例。这个东西很简单,不只能够判断是否直接构造函数实例,还能判断是否父对象构造函数的实例
function Person(){} var p = new Person(); console.log(p instanceof Person); //true console.log(p instanceof Object); //true
js的方法名不能相同,咱们只能模拟实现相似c++同样的多态。
注意:这个名字只是用了强类型语言的说法,js是个解释型语言,没用编译过程。
在方法内部判断参数状况进行重载
//修改字体,仅用部分属性举例: function changeFont(obj, color, size, style){ if(arguments.lenght === 4){ //当传入了参数为4个参数时候作的事情 obj.style.fontSize = size; obj.style.fontColor = color; obj.style.fontStyle = style; return; } if(arguments.length === 2 && typeof arguments[1] === "object"){ //当传入了参数为2个参数时候作的事情 obj.style.fontSize = arguments[1].size || obj.style.fontSize; obj.style.fontStyle = arguments[1].style || obj.style.fontStyle; obj.style.fontColor = arguments[1].color || obj.style.fontColor; return; } throw TypeError("the font cannot be changed..."); }
//构造简单对象 function toObject(val){ if(val === Object(val)) return val; if(val == null) throw TypeError("'null' and 'undefined' cannot be an Object..."); switch(typeof val){ case "number": return new Number(val); case "string": return new String(val); case "boolean": return new Boolean(val); case "symbol": return new Symbol(val); default: throw TypeError("Unknow type inputted..."); } }
java的多态都是编译时多态。因此这个概念是源于c++的,c++利用虚基类实现运行过程当中同一段代码调用不一样的函数的效果。而在js中能够利用函数传递实现运行时多态
function demo(fun, obj){ obj = obj || window; fun.call(this); } function func(){ console.log("I'm coding in " + this.lang); } var lang = "C++"; var o = { lang: "JavaScript", func: function(){ console.log("I'm coding in " + this.lang); } }; demo(func); demo(o.func); demo(func, o);
咱们都知道子对象能够重写父对象中的函数,这样子对象函数对在子对象中替代父对象的同名函数。但若是咱们但愿既在子对象中重写父类函数,有想使用父类同名函数怎么办!分一下几个状况讨论:
//状况1 function Person(){ this.doing = function(){ console.log("I'm working..."); }; } function Coder(){ Person.call(this); var ParentDoing = this.doing; this.doing = function(){ console.log("My job is coding..."); ParentDoing(); } } var coder = new Coder(); coder.doing(); //测试 //状况2 function Person(){ } Person.prototype.doing = function(){ console.log("I'm working..."); }; function Coder(){ Person.call(this); this.doing = function(){ console.log("My job is coding..."); Person.prototype.doing.call(this); }; } var coder = new Coder(); coder.doing(); //测试 //状况3 function Person(){ } Person.prototype.doing = function(){ console.log("I'm working..."); }; function Coder(){ } Coder.prototype = Object.create(Person.prototype); Coder.prototype.constructor = Coder; Coder.super = Person.prototype; Coder.prototype.doing = function(){ console.log("My job is coding..."); Coder.super.doing(); }; var coder = new Coder(); coder.doing(); //测试