JS继承的原理、方式和应用
概要:
一、继承的原理
二、继承的几种方式
三、继承的应用场景
什么是继承?
继承:子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。
要了解JS继承必须首先要了解this的指向,原型prototype、构造器constructor、原型链_proto_;
第一:关于this的指向问题:
// "use strict" //严格模式(strict mode)即在严格的条件下运行,在严格模式下,很多正常情况下不会报错的问题语句,将会报错并阻止运行 //this是什么? JS关键字,在JS中具有特殊意义,代表一个空间地址 //this的指向,是在函数被调用的时候确定的。也就是执行上下文被创建的时候确定的 // 一、初始情况下 // 全局作用域下的this指向:即this和window指向同一个堆内存 this == window; //true this === window; //true // console.log("init," + this) //二、函数和对象里的this的指向 var x = "x"; function test() { var x = 1; // this.x = 2; console.log(this); console.log(this ? this.x : ''); } //直接调用
// 严格模式下,如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象;如果函数独立调用,那么该函数内部的this,则指向undefined。
test(); // 非严格模式下调用相当于window.test(), this指向window;严格模式下this等于undefine
var y = 2; var Arrow = () => { y = 3; console.log("arrow," + this); console.log(this ? "arrow," + this.y : ''); }; Arrow(); //箭头函数条用时指向上下文this指向window,严格模式下也是指向window console.log("------------------------函数自调用------------------------") //赋值调用 var obj = { x: 1, fn: test, fns: { fn: test, x: "FNC" }, y: 3, Arrow: Arrow, fnArrow: { Arrow: Arrow, y: 5 } } var fn = obj.fn; //定义变量相当于在window对象添加一个属性,window下调用给,他的上层对象就是window,所以this指向window fn(); // 调用的是window.fn(), this指向window。严格模式下为undefined obj.fn() //相当于window.obj.fn();this指向上层对象:obj;严格模式下也是指向上层对象 obj.fns.fn(); //this指向上层对象:fns;严格模式下也是指向上层对象 console.log("------------------------对象里面调用函数------------------------") obj.Arrow(); //严格模式下this指向window obj.fnArrow.Arrow(); //严格模式下this指向window console.log("------------------------对象里面调用箭头函数------------------------") //三、构造函数的this指向,构造函数在new实例化的过程中改变了this的指向,所以this指向当前函数的实例对象 function Structure() { this.user = "二营长"; console.log(this) } var StructureArrow = () => { console.log(this) } var a = new Structure(); console.log(a.user); //二营长 console.log("type," + Object.prototype.toString.call(StructureArrow)) // var sa = new StructureArrow(); //StructureArrow is not a constructor; new关键字只能实例化有prototype的函数对象,所以这里抛出异常 // 这里之所以对象a可以点出函数Structure里面的user是因为new关键字可以改变this的指向,将这个this指向对象a, // 为什么我说a是对象,因为用了new关键字就是创建一个对象实例, // 我们这里用变量a创建了一个Structure的实例(相当于复制了一份Structure到对象a里面),此时仅仅只是创建,并没有执行, // 而调用这个函数Structure的是对象a,那么this指向的自然是对象a,那么为什么对象a中会有user, // 因为你已经复制了一份Structure函数到对象a中,用了new关键字就等同于复制了一份。//四、箭头函数的this指向 console.log("------------------------箭头函数的this指向------------------------") var ar = 0; function arrows() { var ar = 1; var f = () => { console.log(this); console.log(this ? this.ar : ''); } return f(); } var arrowsobj = { ar: 2, fn: arrows, fn1: () => { console.log(this); //箭头函数将this指向当前环境上下文,即this指向全局环境中的this,即window console.log(this ? this.ar : ''); //0 箭头函数将this指向当前环境上下文,即this指向全局环境中的this,即window }, fn2: function() { setTimeout(() => { console.log(this); //指向arrowsobj console.log(this ? this.ar : ''); //2 }, 0) }, fnc: { fn1: () => { console.log(this); //箭头函数将this指向当前环境上下文,即this指向全局环境中的this,即window console.log(this ? this.ar : ''); //0 箭头函数将this指向当前环境上下文,即this指向全局环境中的this,即window } } }; arrows(); //非严格模式下为this.ar 为0;严格模式下报错 arrowsobj.fn(); //this.ar 为2;严格模式下报错箭头函数将this指向当前环境上下文,即this指向test中的this,即arrowsobj // arrowsfn(); arrowsobj.fn1(); arrowsobj.fn2(); arrowsobj.fnc.fn1(); console.log("------------------------事件里面的this指向------------------------") //五、事件里面的this指向 //事件绑定 var btn = document.querySelector("body"); btn.onclick = function() { console.log(this) // 调用的是btn.onclick, this指向body } //事件监听 var btns = document.querySelector("html") btns.onclick = function() { var timers = setTimeout(() => { console.log(this) //上下文对象是body }, 1000); } // this指向window // 全局作用域下的this都是:即this和window指向同一个堆内存; // 自执行函数中的this都是 // 函数作为参数里面的this一般都是 // 回调函数中的this指向; // 递归函数中的this指向; // this不是window // 函数执行的时候判断函数前面是否有点,如果有点,前面是谁this就是;如果没有点this就是; // 给当前元素某个事件绑定方法,元素触发事件,事件中的; // 构造函数中的; // 通过call apply bind可以改变this指向,指向谁就是谁; // 箭头函数中没有this;
第二、prototype(JS对象)
javascript中的每个对象都有prototype属性,Javascript中对象的prototype属性的解释是:返回对象类型原型的引用。
每一个构造函数都有一个属性叫做原型。这个属性非常有用:为一个特定类声明通用的变量或者函数。
你不需要显式地声明一个prototype属性,因为在每一个构造函数中都有它的存在。
在JavaScript中,prototype对象是实现面向对象的一个重要机制。
每个函数就是一个对象(Function),函数对象都有一个子对象 prototype对象,类是以函数的形式来定义的。prototype表示该函数的原型,也表示一个类的成员的集合。
第三、constructor (构造器,指向创建自己的那个构造函数)
在JavaScript中,每个具有原型的对象都会自动获得constructor属性。除了arguments、Error、Global、Math、RegExp、Regular Expression、Enumerator等一些特殊对象之外,其他所有的JavaScript内置对象都具备constructor属性。例如:Array、Boolean、Date、Function、Number、Object、String等。所有主流浏览器均支持该属性
第四、原型链_proto_(每一个对象都有一个_proto_属性,这个属性指向一个对象,这个对象是原型对象)
由__proto__组成的链条叫做原型链 (访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着_proto_这条链向上找,这就是原型链。
// 在js中万物皆对象,对象又分为两种:普通对象(Object)和函数对象(Function)。 // prototype:每一个函数都有一个prototyp属性 这个属性指向一个对象 这个对象叫做原型对象; //constructor:构造器,指向创建自己的那个构造函数 // __proto__:每一个对象都有一个_proto_属性,这个属性指向一个对象,这个对象是原型对象 // 原型对象里面有2个属性:constructor,__proto__ // 任何对象都具有隐式原型属性(__proto__),只有函数对象有显式原型属性(prototype)。 // 原型链:由__proto__组成的链条叫做原型链 (访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着_proto_这条链向上找,这就是原型链。) function fn() { console.log("fn") } console.log(fn); console.dir(fn); // 一.函数对象和一般对象 var Literal = function() { console.log("Literal") } console.log(Literal) //输出函数自身,字符串 console.dir(Literal) //输出对象 var f = new Function("arg", "statement"); console.dir(f); //注意:由上面两条得出,字面量函数和关键字函数都拥有这两个特殊的属性__proto__和prototype var newLiteral = new Literal(); console.log(newLiteral) //输出对象 console.dir(newLiteral) //输出对象 //new关键字的构造函数,相当于实例化一个对象,将实例的函数重新拷贝一份,所以只有__proto__ var obj = { fn: function() { console.log("2") }, //拥有__proto__和prototype newfn: new fn(), //new关键字的构造函数,相当于实例化一个对象,将实例的函数重新拷贝一份,所以只有__proto__ obj: {}, //__proto__ arr: [], //__proto__ x: '6', type: null }; console.log(obj); // new一个对象时,会经历以下几个步骤(摘自javascript高级程序设计): // (1)创建一个对象; // (2)将构造函数的作用域赋值给新对象(因此this就指向了这个新对象); // (3)执行构造函数中的代码(为这个新对象添加属性); // (4)返回新对象 // 创建了一个全新的对象。 // 这个对象会被执行 [[Prototype]](也就是 proto)链接。 // 生成的新对象会绑定到函数调用的 this。 // 通过 new创建的每个对象将最终被 [[Prototype]]链接到这个函数的 prototype对象上。 // 如果函数没有返回对象类型 Object(包含 Functoin, Array, Date, RegExg, Error),那么 new表达式中的函数调用会自动返回这个新的对象。 Object.prototype.__proto__ == null Object.__proto__ == Functon.prototype Function.protype.__proto__ == Object.prototype Function.__proto__ == Function.prototype // 原型链都指向了null对象,也就是正常的结束,不会死循环
他们之间的关系图:
二、继承的几种方式
1.原型链继承(类式继承): 将父函数的实例继承给子函数prototype原型属性;
2.借用构造函数继承: 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(跟原型没有任何关系)
3.组合式继承: 结合了原型链继承和构造函数继承两种模式的优点,传参和复用,在子类构造函数中执行父类构造函数,在子类
原型上实例化父类。(最常用)
4.原型式继承 : 封装一个函数,该函数返回一个不含实例属性的对象,然后对这个对象逐步增强(逐步添加实例属性)
5.寄生继承:创建对象-增强-返回该对象,这样的过程叫做寄生式继承(寄生式继承和原型式继承是紧密相关的一种思路。寄生式继承就是给原型式继承穿了个马甲而已)
6.寄生组合式继承:通过寄生方式,砍掉了子类原型对象上多余的那份父类实例属性,这样,在调用两次父类的构造函数的时候,就不会初始化两次实例方法/属性,避免了组合继承的缺点(最理想)
7.class类继承:consuctor的super方法;
第一、类式继承(原型链继承)
//一、类式继承(原型链继承) //声明父类 function Parent() { var sex = "man"; //私有属性 var height = [178]; //私有引用属性 function sleep() { console.log("sleep") } //私有函数(引用属性) this.books = ["javascript", "css", "html"]; this.ParentInfo = true; //实例属性 this.height = [179]; //实例引用属性 this.sleep = function() {}; //实例函数(引用属性) this.name = "父类"; } Parent.prototype.getParentBaseinfo = function() { return this.ParentInfo; } Parent.addClass = "human"; //私有属性,子类继承后会在Child.prototype.__proto__.constructor里面 //声明子类 // console.dir(Parent); function Child() { this.ChildInfo = false; return this; } // Parent(); //继承父类 Child.prototype = new Parent(); Child.prototype.getChildBaseinfo = function() { return this.ChildInfo; } console.dir(Child); // 继承父类之后,子类可以使用父类的实例属性以及父类的原型属性 console.log(Child.prototype.getParentBaseinfo(), Child.prototype.__proto__.constructor.addClass) // Child.prototype.__proto__.constructor==Parent console.log(new Parent()) // 缺点1、由于子类通过其原型prototype对父类实例化,继承了父类,所以说父类的共有属性要是引用类型,就会在之类中被所有实例共用 //因此一个子类的实例更改子类原型从父类构造函数中继承来的属性就会直接影响到其他子类 //如下所述 var instance1 = deepClone(new Child()); var instance2 = new Child(); console.log(instance1, instance2, instance2.books) //["javascript", "css", "html"] instance1.books.push("设计模式"); console.log(instance1, instance2.books) // ["javascript", "css", "html", "设计模式"] // 解决方案:深拷贝=>但是每次实例化的时候都需要进行深拷贝比较麻烦 function deepClone(initalObj, finalObj) { var obj = finalObj || {}; for (var i in initalObj) { var prop = initalObj[i]; // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况 if (prop === obj) { continue; } if (typeof prop === 'object') { obj[i] = (prop.constructor === Array) ? [] : {}; arguments.callee(prop, obj[i]); } else { obj[i] = prop; } } return obj; } // 缺点2,无法向父类构造函数传递参数,由于子类实现继承是靠其原型prptotype对父类的实例化实现的,因此创建父类的时候,是无法向父类传递参数的,
//因此在实例化父类的时候, // 也无法对父类构造函数内的属性进行初始化 console.log("--------------------传递参数示例---------------------") function Person(name, age, job) { this.name = name; this.age = age; this.ob = job; } function Man(age) { this.name = age; } var m = new Man('Anthony', 27, 'PE'); m.prototype = new Person('thony', 27, 'PE'); var v = new Man('maker', 32, 'TW'); v.prototype = new Person('Alice', 21, 'VN'); console.dir(m); console.dir(v); //优缺点 //1.优点: // 从已有的对象衍生新的对象,不需要创建自定义类型 // 2.缺点 // (1).新实例无法向父类构造函数传参( // 并不是语法上不能实现对构造函数的参数传递,而是这样做不符合面向对象编程的规则:对象(实例)才是属性的拥有者。 // 如果在子类定义时就将属性赋了值,就变成了类拥有属性,而不是对象拥有属性了。) // (2).原型引用属性会被所有实例所共享,因为是整个父类对象充当子类的原型对象,所以这个缺陷无法避免、 // (3).无法实现代码的复用 // (4).继承单一 //类的原型对象的作用就是为类的原型添加共有的方法,打包类不能直接访问这些属性和方法,必须通过原型prototype来访问。而我们实例化一个函数的时候,
//新创建的对象复制了父类构造 // 函数的属性和方法并且将原型__proto__指向父元素的原型对象,这样就拥有了父类原型对象上的属性与方法。
二、借用构造函数继承
//声明父类 function Parent(id) { this.books = ['JavaScript', 'html', 'css']; this.id = id || ''; // this.showBooks = function() { // console.log(this.books); // } } function Monther(id) { // this.books = ['UI', 'JAVA']; this.id = id || ''; this.hobby = ['draw', 'movie'] } //父类声明原型方法 Parent.prototype.showBooks = function() { console.log(this.books); } //声明子类 function Child(id) { console.log("call改变原实例的this指向", this) Parent.call(this, id); //call改变父类this作用域名的指向 Monther.call(this, id); //call改变母类this作用域名的指向,但是相同属性后者会覆盖前者 } var test1 = new Child(11); var test2 = new Child(12); test1.books.push("设计模式"); // console.dir(Parent) // console.dir(Monther) console.log("---------------------输出测试实例1----------------------") console.log(test1); console.log(test1.books); console.log(test1.id); console.log("---------------------输出测试实例2----------------------") console.log(test2); console.log(test2.books); console.log(test2.id); // test1.showBooks(); //Parent.call(this,id)是构造函数式的精髓,由于call这个方法可以更改函数的作用环境, // 因此在子类中,对Parent调用这个方法,就是将子类的变量在父类中执行一遍, // 由于父类中是给this绑定属性的,因此子类自然就继承了父类的共有属性。由于这种类型的继承没有涉及prototype, // 所以父类的原型方法自然就不会被子类继承。 //如果想要继承父类的原型方法就必须绑定在this上面,这样创建出来的每一个实例都会单独拥有一份而不能共用, // 这样就违背了代码复用的原则。 //为了综合之前两种模式的有点于是有了组合式继承
//三、组合式继承//声明父类 function Parent(name) { this.books = ['JavaScript', 'html', 'css']; this.name = name; // this.showBooks =function(){ // console.log(this.books); // } } //父类声明原型共有方法 Parent.prototype.getName = function() { console.log(this.name); } //声明子类 function Child(name, time) { //构造函数式继承父类name属性 Parent.call(this, name); //call改变父类this作用于的指向,从父类拷贝一份父类的实例属性给子类作为子类的实例属性 this.time = time; } Child.prototype = new Parent(); //创建父类实例作为子类的原型 ,此时这个父类实例就又有了一份实例属性,但这份会被第一次拷贝来的实例属性屏蔽掉 Child.prototype.getTime = function() { console.log(this.time) } //在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式,这样就融合了类式继承和构造函数的优点,并且过滤掉其缺点。 var fn = new Child('js book', '2018-12-14'); console.dir(fn) fn.books.push("设计模式"); console.log(fn.books); //["JavaScript", "html", "css", "设计模式"] fn.getName(); fn.getTime(); var fnc = new Child('css book', '2019-10-24'); console.log(fnc.books); // ["JavaScript", "html", "css"] fnc.getName(); //css book fnc.getTime(); //2019-10-24
//原型式继承 function inheritObject(o) { //声明一个过渡函数对象 function F() {} //过渡对象的原型继承父对象 F.prototype = o; //返回过渡对象的一个实例,该实例的原型继承了父对象 return new F(); } var book = { name: "js book", alikebook: ["css book", "html book"] }; var newBook = inheritObject(book); newBook.name = "node book"; newBook.alikebook.push("jquery book"); var otherBook = inheritObject(book); otherBook.name = "flash book"; otherBook.alikebook.push("flash book"); console.log("-------------------输出newBook---------------------------") console.dir(newBook) console.log(newBook.name) //node book console.log(newBook.alikebook) //["css book", "html book", "jquery book", "flash book"] console.log("-------------------输出otherBook---------------------------") console.dir(otherBook) console.log(otherBook.name) //flash book console.log(otherBook.alikebook) // ["css book", "html book", "jquery book", "flash book"] console.log("-------------------输出book对象属性---------------------------") console.log(book.name) //js book console.log(book.alikebook) //["css book", "html book", "jquery book", "flash book"] //原型继承,跟类继承一样,父类对象book中的值被复制,引用类型的属性被共用
function inheritObject(o) { //声明一个过渡函数对象 function F() {} //过渡对象的原型继承父对象 F.prototype = o; //返回过渡对象的一个实例,该实例的原型继承了父对象 return new F(); } //声明函数对象 var book = { name: "JS Book", alineBooks: ["Css Book", "Html Book"] }; function Parent() { var sex = "man"; //私有属性 var height = [178]; //私有引用属性 function sleep() { console.log("sleep") } //私有函数(引用属性) this.books = ["javascript", "css", "html"]; this.ParentInfo = true; //实例属性 this.height = [179]; //实例引用属性 this.sleep = function() {}; //实例函数(引用属性) this.alineBooks = ["Css Book", "Html Book"] this.name = "父类"; } Parent.prototype.getParentBaseinfo = function() { return this.ParentInfo; } var par = new Parent() function createBook(obj) { var o = new inheritObject(obj); // console.log(o) o.getName = function() { console.log(name); }; return o; } var getBook = createBook(par); //函数生命呢之后可以添加其他属性 getBook.setname = function() { this.name = "Java Book" } var getnewBook = createBook(par); getBook.name = "Node Book"; getBook.alineBooks.push("PDF Book") console.log("-----------------------getBook----------------") console.dir(getBook) console.log(getBook.name); console.log(getBook.alineBooks); console.log("-----------------------getnewBook----------------") console.dir(getnewBook) console.log(getnewBook.alineBooks) console.log("-----------------------book----------------") console.dir(par) // console.log(book.name); // console.log(book.alineBooks);
//寄生组合式继承(寄生式+原型:通过借用函数来继承属性,通过原型链的混成形式来继承方法) function inheritObject(o) { //声明一个过渡函数对象 function F() {} //过渡对象的原型继承父对象 F.prototype = o; //返回过渡对象的一个实例,该实例的原型继承了父对象 return new F(); } function inheritPrototype(Child, Parent) { //复制一份父类的原型副本保存在变量中 var p = inheritObject(Parent.prototype); //修正因为重写子类的原型导致子类的constructor属性被修改 p.constructor = Child; //设置子类的原型 Child.prototype = p; } //定义父类 function Parent(name) { this.name = name; this.colors = ["red", "blue", "green"]; } //定义父类的原型方法 Parent.prototype.getName = function() { console.log(this.name); } function Child(name, time) { //构造函数式继承 Parent.call(this, name); this.time = time; } //寄生式继承父类原型 inheritPrototype(Child, Parent); //子类新增原型方法 Child.prototype.getTime = function() { console.log(this.time); }; //创建两个测试方法 var test1 = new Child("js book", "2018-01-02"); var test2 = new Child("css book", "2018-01-03"); test1.colors.push("black") test2.getName() //css book test2.getTime() //2018-01-03 console.dir("-----------------test1--------------------") console.dir(test1) console.log(test1.colors); //["red", "blue", "green", "black"] console.dir("-----------------test2--------------------") console.dir(test2) console.log(test2.colors); // ["red", "blue", "green"]
class Parent{ //属性 constructor(name,age){ this.name = name; this.age = age; } eat(){ console.log('111') } show(){ console.log('222') } } //ES6的继承 class Man extends Parent{ constructor(beard,name,age){ super(name,age)//super调用父类的构造方法! this.beard = beard; } work(){} } var p2 = new Man(10,"张家辉",40); var p1 = new Man(10,"古天乐",41); console.log(p1,p2) //优缺点,代码简洁,但是有兼容性问题
三、JS继承的应用场景
JS继承的话主要用于面向对象的变成中,试用场景的话还是以单页面应用或者JS为主的开发里,因为如果只是在页面级的开发中很少会用到JS继承的方式,与其说继承,还不如直接写个函数来的简单直接有效一些。
想用继承的话最好是那种主要以JS为主开发的大型项目,比如说单页面的应用或者写JS框架,前台的所有东西都用JS来完成,整个站的跳转,部分逻辑,数据处理等大部分使用JS来做,这样面向对象的编程才有存在的价值和意义
为什么要继承:通常在一般的项目里不需要,因为应用简单,但你要用纯js做一些复杂的工具或框架系统就要用到了,比如webgis、或者js框架如jquery、ext什么的,不然一个几千行代码的框架不用继承得写几万行,甚至还无法维护