跟传统面向对象语言比起来,js在继承关系方面比较特别,若是第一次看恐怕会有些抓狂,偶就是这样(又透露小白本质#=_=),从哪里提及好呢?函数调用?php
js中函数的调用方式大体可分如下几种:java
1. 普通函数,直接调用ios
function Hi(){ alert(233); } Hi(); var f = function(){ alert(666); }; f();
2. 做为对象的方法调用c++
var obj = { x:1, m:function(){ alert("hello"); } }; obj.m();
3. 构造函数调用,即new 表达式chrome
var a = new Array(1,2,3);
4. 间接调用,最经常使用应该是call或apply了数组
function f(){ alert(this.name); } var obj = { name:"Apple" }; f.call(obj); // 间接调用函数f,输出Apple
顾名思义就是非直接的调用一个函数了,这里的f.call(obj)的至关于obj.f(),若是直接写obj.f()确定是错误的,因obj对象压根没定义这个方法,这即是奇妙的地方:obj对象经过call间接调用了f()函数。另外一个跟call类似的函数是apply,道理相同。浏览器
仍是要牢记在js中除了null和undefined外都是对象,它们的基类都是Object,Object是全部对象的祖宗(比如六道仙人他妈),函数天然也是对象,并且js中全部的函数会自动继承Function这个类(固然最终继承自Object),在咱们写js代码时,环境中已经有了这些类,Function这个类里面就定义了一些方法,其中就有call和apply,因此f才能打点这么调。app
call与apply方法的第一个参数是调用者所在的对象,好比这里想达到让obj来调用f()的目的,在f()函数中要对obj对象的属性进行引用,那么第一个参数就传入obj这个对象,这样f()函数中的上下文就是(context,好比全局做用域在浏览器中上下文是window对象,this是对它的引用)obj这个对象,因此在f()函数中this.name引用的是obj的name属性,这个this即是对obj的引用。函数
若是f()函数须要传参数的话,用call这种间接调用的方式怎么写?这即是call与apply的不一样之处了,假如如今f()是这样的:f(name, age){...},那么这两种方式是:工具
call写法:f.call(obj, "Li", 23);
apply写法:f.apply(obj, ["Li", 23]);
很明显的区别:call以单个参数形式、逗号分隔传入,有几个写几个,放在第一个对象参数后面,apply则把全部要传入的参数统一放在一个数组中。
有时调用apply第一个对象参数没有或没必要要,能够传undefined或null,并且在全局做用域中用这种间接形式调用时,第一个参数能够是全局对象引用this,或者直接写undefined时,它会自动去把这个全局下文环境做为第一个参数对象,所以咱们日常的写的函数能够有不少奇怪的写法:
// 通常写法 Math.max(1, 5, 7, 2); // 奇怪写法 Math.max.call(Math, 1, 5, 7, 2); Math.max.call(window, 1, 5, 7, 2); Math.max.call(this, 1, 5, 7, 2); Math.max.call(undefined, 1, 5, 7, 2); Math.max.apply(Math, [1, 5, 7, 2]); // max函数可传入不定个数参数
大概设计者想让天下没有难调的方法,因此出这么一招,这样的话,理论上任何对象的可调方法,几乎都能被其余无关的对象调用。
js有面向对象,却没有直接定义的类(第一次听很神奇),对象的实现由构造函数加调用表达式完成
function Game(name){ this.name = name; } var g = new Game("2048");
在调用new的时候,对象已经造成,接着运行Game函数,Game函数内部的上下文就变成了这个实例化对象的环境,this即是对这个实例化对象的引用,因此Game构造函数里边的语句只是在对这个对象进行一下组装,就比如:
var g = {}; g.name = "2048"; // 对象已生成,这里对它设置一些属性
js的继承方式大体分两种,第一种就是像日常那些语言(php、c++、java等),好比class A extends B{...},class A : public B{...},经过对父类的继承,称为类式继承。通常来讲这种方式会在子类构造函数中调用父类构造函数(不是必须),以初始化父类对象中的一些属性、方法等,js同理也会这样
function A(){ this.a = "A"; } function B(){ this.b = "B"; A.call(this); // 继承A,调用它的构造函数 } var bObj = new B(); console.log(bObj.a); // A console.log(bObj.b); // B
这里便用到了间接调用,并且通常也会这么写,由于须要传入子对象的上下文this引用。父类构造函数A是个函数对象,它的调用只是在子类加了些东西进来而已,或者说初始化父类的属性,它的效果跟下面同样:
function B(){ this.b = "B"; var self = this; function A(){ self.a = "A"; } // 直接定义一个函数,为这个对象添加属性 A(); // 调用它 } var bObj = new B(); console.log(bObj.a); // A console.log(bObj.b); // B
类式继承就这么简单,调用了下就表示继承了另外一个类,但并不经常使用。它的好处是能够在子类实例化时直接给父类的构造函数传参
function A(platform){ this.platform = platform; } function B(name, platform){ this.name = name; A.call(this, platform); // 传参给父类 } var bObj = new B("2048", "ios");
第二种是原型链继承,首先要大概知道原型(prototype)是什么。在js中每个函数都关联着一个prototype属性,这是语言自己的机制,这个prototype会指向一个对象,这个对象存放着一些方法和属性,若是B构造函数的prototype属性被赋值为A构造函数建立的实例,就完成了一次原型式继承:B的实例继承自A,相似下面child继承Parent(例1)
function Parent(name){ this.name = name; } function Child(age){ this.age = age; } Child.prototype = new Parent("Li"); var child = new Child(25); console.log(child.name); // Li console.log(child.age); // 25
Child.prototype = new Parent("Li")就表示Child的实例化对象继承了Parent这个类,prototype这种继承形式被不少人诟病,但它确实有强大的一面。例如咱们都知道对象类型的变量是引用,引用是共享地址的,一个的属性值改变了,另外一个的值跟着变,以下
function Game(){ this.name = "2048"; } var o1 = new Game(); console.log(o1.name); // 2048 var o2 = o1; o2.name = "gta"; console.log(o1.name); // gta
当从Game建立对象时,两个对象的方法是彻底分开的,各有一份
function Game(){ this.name = "2048"; this.m = function(name){ this.name = name; } } var o1 = new Game(); var o2 = new Game(); console.log(o1.m == o2.m); // false,两个对象的方法不相等 o1.m("gta"); o2.m("angry bird"); console.log(o1.name); // gta console.log(o2.name); // angry bird,o一、o2各改各的值
但在原型中定义的属性
// 以上例为基础 Game.prototype.f = function(){ this.name = "unknown"; }; console.log(o1.f === o2.f); // true,o一、o2的f方法相等
即在prototype中定义的属性,是被全部实对象共享的,普通对象的属性,每实例化一次,它们的属性、方法就被复制一份存到各自的对象里面去,但prototype中定义的只有一份,由于一个构造函数只关联一个prototype,而全部的对象从这个构造函数实例化而来。故理论上全部函数都可做为构造函数并用做原型继承(固然还有一点限制),他们都有prototype属性。
那么当具体访问对象的某一属性时,如何找到?好比o1.name,首先在o1对象中找,当它本身对象中没有该属性时,他会到它的构造函数的原型---Game.prototype所指向的对象中去找,而Game.prototype又是由另外一个构造函数设为A,实例化获得的对象,这个对象对应的构造函数A也有本身的prototype---A.prototype,当在Game.prototype中没找到时,又会去A.prototype对中去找,直到最终的老祖宗Object的prototype---它是undefined,此时如还未找到就会报错,找到了的话在哪一个里面就用哪一个。这种一级一级的、回溯的prototype造成的链式结构被称为原型链,这种方式也被称为原型链继承。
function Animal(){ this.name = 'animal'; } function Dog(){ this.type = 'dog'; } Dog.prototype = new Animal(); // 原型继承 function Husky(){ this.weight = 2.3; } Husky.prototype = new Dog(); var o = new Husky(); // 顺着原型链查照 console.log(o.name); console.log(o.type); console.log(o.weight);
原型的第一大优势就是属性(方法)只保留一份,各对象共享,节省空间,还有个优势是能够动态的添加属性。上例中,o一、o2已经实例化了,在函数对象Game的prototype属性上添加其余值时,对象依然能够访问到,缘由就是先找本身对象中的属性,再顺着原型链找,原型中有那就用它了。
只有函数才有prototype属性且能够访问到,其余对象实例是没有的,但其余对象实例有一个__proto__,听说它是一个内在的指针,指向对象所继承的类,不少浏览器都视其为私有属性而不可访问,貌似火狐将其暴露了出来。在chrome的控制台中能够大体能够看到这种风结构
对象的__proto__指向Object,数组的__proto__指向Array,正由于如此,咱们才能调用到一些对象的方法,好比var a = [1,2,3]; a.push(4);,由于push已经被封装在Array里面了,它们在调用属性、方法时也会顺着__proto__这个链去找。
原型或者说原型对象,是类的惟一标识,也就是说当两个对象继承自同一个原型对象时,它们才属于同一个类的实例。当两个构造函数的prototype是指向同一个对象时,由它们建立的实例才是同类的,如何判断对象是否是属于某个类?可用instanceof运算符,如
o instanceof Husky
若是o继承自Husky.prototype(注意不是Husky),则返回true。既然这样为什么不直接写 o instanceof Husky.prototype ?结果报错了:instanceof右边必须是个函数。另外一种方法是isPrototypeOf,如
Husky.prototype.isPrototypeOf(o)
若o为Husky的实例化对象则返回true,Husky.prototype是o的原型。
若是观察了chrome的控制台打印对象的话,特别是经过new表达式生成的对象,就会发如今对象的原型__proto__所指的对象中有一个constructor属性,这个属性的值就是它本身的构造函数(对象)
对于一个完整的原型对象来讲,它的constructor属性是它自己的构造函数对象。因此更好的处理是这样的
function Animal(){ this.name = "animal"; } function Dog(){ this.type = "dog"; } Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; // 将原型constructor属性指向本身的构造函数
这里有几个容易让人糊涂的东西:构造函数对象、实例化对象、原型对象。梳理一下:
稍微标准点的原型式继承不是直接B.prototype = new A(),而是封装到函数里面,以下
function inherit(obj){ // 返回继承自对象obj的新实例 if(obj === null) return null; if(Object.create) // 使用ECMAScript5函数 return Object.create(obj); var t = typeof obj; if(t != "object" && t != "function") return null; function f(){} // 定义一个空函数 f.prototype = obj; return new f(); // 返回继承原型的实例对象 } var obj = { x:5, m:function(){console.log(this.x);} }; var subObject = inherit(obj); // 子对象
这里用到了Object.create()函数,它的第一个参数是对象,若是只传第一个参数如obj,它将返回指定原型为obj对象的新对象。若是Object.create不存在,建立一个空函数,让函数的prototype指向obj,再返回以这个空函数的实例对象,也完成了原型式继承(有的把这个称做寄生式继承)。再看下例
function A(){ this.name = "A"; } function B(){ this.type = "B"; } B.prototype = inherit(A.prototype); B.prototype.constructor = B; // 将constructor指向本身的构造函数 var o = new B();
o是B的实例,B是A的子类,则需保证B的原型对象继承自A的原型对象,若是不这样,B的原型只继承自Object.prototype,那么o跟一个普通实例没有差异。OK,这种写法,o里面有没有属性name?事实是没有。前面在说类式继承时,父类构造函数在子类构造函数里面调用一下,其实至关因而给子类实例添加了属性,因此子类实例才拥有这个属性,这里B的prototype只继承自A的prototype,也不是前面的原型继承时写的B.prototype = new A()(这样写o.name是存在的),都是原型继承,两种不一样写法形成了这一差异。因此原型继承的核心仍是在于:o的构造函数对象B的prototype原型到底包没包含所需的属性。
针对以上类式继承和原型式继承各自的优缺点,又来了个新方式:组合继承。组合固然是兼具类式继承和原型式继承的优势了。
function Parent(){ this.name = "parent"; } function Child(){ this.type = "child"; } // 先处理原型链 Child.prototype = new Parent(); Child.prototype.constructor = Child; // 而后添加需继承的属性和方法 Child.prototype.p = "2333"; Child.prototype.f = function(){ console.log("hello"); }; var o = new Child();
以上只是零散的组合先设置好原型链,而后手动给原型添加属性和方法,代替类式继承的直接传参,以此克服弊端。若是写的标准点,首先准备个给原型添加属性的小函数:
// 将对象q的属性添加到p中 extend(p, q){ if(p == null || q == null) return; if(typeof p != "object" || typeof q != "object") return; for(var prop in q) p[prop] = q[prop]; }
另外一个工具工具函数
/* * 实现子类的继承 * @param superClass 父类构造函数对象 * @param subClass 子类构造函数对象 * @param methods 包含函数属性的对象 * @param props 包含属性的对象 * return function 组合继承后的构造函数 */ function inheritClass(superClass, subClass, methods, props){ // 使子类在原型上继承自父类 subClass.prototype = inherit(superClass.prototype); subClass.prototype.constructor = subClass; // 将所需方法添加至子类原型中 if(typeof methods == "object") extend(subClass.prototype, methods); // 将所需属性添加至子类原型中 if(typeof props == "object") extend(subClass,.prototype, props); // 返回子类 return subClass; }
superClass是父类构造函数,subClass是子类构造函数,methods与props为将要添加的方法和属性的集合,以对象的方式包装起来。借用了inherit和extend方法,inherit方法设置好原型链,为了克服没法直接传参的弊端,将方法和属性包装好后,动态的添加到子类构造函数的prototype中。
如inheritClass方法添加方法和属性时,这样写extend(subClass, methods),将给subClass这个函数对象添加的是私有属性,对象打点没法访问,只有构造函数打点能够访问,在其余语言中,静态变量或常量相似这种形式,因此这些属性在js中称为类的私有属性。
测试代码打印下看效果,依然在chrome控制台
function Parent(){ this.p = "parent"; } function Child(){ this.c = "child"; } // 添加的方法对象 var mObj = { sayHello:function(){ console.log("hello") } }; // 添加的属性对象 var pObj = { pos:"developer" }; // 实现继承 var InheritedChild = inheritClass(Parent, Child, mObj, pObj); var obj = new InheritedChild(); console.log(obj);
console.table打印一个树形结构,obj是对象,因此是__proto__,它关联给本身实例化的构造函数的原型对象,表现上就是__proto__: Child,这个原型中有constructor属性,值为Child函数对象,还有添加进去的pos属性和sayHello方法,这个原型又关联这个Parent的prototype原型对象:__proto__:Parent,同理这个原型有本身的constructor属性,并最终继承自Object。
继承关系上大体就是这样,乱七八糟一堆,也许一个$.extend()方法就解决了~