JS高级—08—原型;原型链;如何实现js的继承;
原型帮助我们少写实例方法;
原型链帮助我们实现继承;
目前常用的寄生组合式继承主要三步:
- 1.父类实例对象等于子类原型
- 2.借用构造函数
- 3.原型式继承(一个继承父类原型的新对象,替换父类实例对象)
一、原型(隐式原型、显式原型)(对象原型、函数原型)
原型分为两种,分别是
-
隐式原型,也就是对象的[[prototype]]属性,也可以叫对象原型
-
显式原型,也就是函数的protutype属性,也可以叫函数原型;函数原型指向的对象就是函数原型对象,每一个通过new构造函数创造出的实例对象的隐式原型也指向这个实例对象;
- 注意:因为在js中所有东西都是对象,函数也是一种特殊的对象,所以函数也有隐式原型;
1.1对象原型
js中规定了每个对象都要有内置[[prototype]]属性,也叫隐式原型;
隐式原型的作用:
let obj = { } obj.__proto__.age = 18; //obj没有age属性,我们这个通过隐式模型去给他设置一个属性,看后面能不能获取到。 console.log(obj.age) //18;age对象自己属性里没有,会去隐式原型中查找,获取到了。
浏览器给每个对象加了一个__proto__属性,这个属性可以指向对象的隐式原型;即obj.__proto__ === obj.[[prototype]](当然了内置属性是不能访问的,所以obj.[[prototype]]这种写法有问题,这里这样写只是方便理解;)
但是这个__proto__属性是某些浏览器自己加的,并不在ecma规范,所以测试环境可以使用,生产环境可能面临客户使用不同浏览器的情况,生产环境不要用;
注意!!!
使用Object.getProtoTypeOf和Object.setPtotoTypeOf才是ecma规范定义的获取或设置对象原型的方法,__proto__ 只是某些浏览器自己实现的,并不是规范,生产用规范。
1.2函数原型
函数的prototype属性(显式原型);
根据ecma规范,函数也是一个对象,所以也有隐式原型属性;
根据ecma规范,函数有显式原型属性,显式原型可以直接通过foo.prototype属性调用,这个是和隐式原型不同的点;
在我们new一个构造函数时,js引擎内部会自动帮助我们做,将函数的prototyp属性所指向的内存地址赋值给实例对象的内置[[prototype]]属性,所以实例对象的内置[[prototype]]属性将会指向函数的原型对象;
简言之,实例对象的隐式原型等于函数的显式原型;
如果是一个普通函数,那么调用的时候创建执行函数上下文等;
返回的函数,fn1和fn2的堆内存地址也是不一样的,因为每次调用普通函数时,都会重新把foo函数压入ecs栈,然后都会重新创建一个ao对象,ao对象不一样,所以返回的bar函数的地址也不一样;
但如果用new调用,将会创建一个对象巴拉巴拉;每一个实例对象都是不同的,在堆内存中都有不同的地址,这个是ecma规范;
因为p1和p2是不同的实例对象,所以p1的eating方法、running方法和p2的eating方法、running方法在堆内存中地址也是不一样的;
但是这样就会造成浪费,毕竟eating方法、running方法里很多代码都是一样的;能不能只定义一次,然后所以子都可以使用呢?
解决办法:函数原型。说白了,就是通过函数原型的方式实现继承和多态;
函数的原型是每个实例对象的父亲,实例对象在自己作用域里找不到的方法,就去父亲中找即实例对象的原型也就等于构造函数的原型也就等于函数原型对象,找到后多态,父亲方法中的每个this表示的都是自己这个实例对象。为什么this指向调用它的实例对象,因为这个事this的4项绑定规则之隐式绑定,通过哪个调用时指向谁呀;
1.3函数原型的constructor属性
1.4创建对象的第4中方式
除了new Object、字面量创建、构造函数创建
还可以使用构造函数+原型的方式创建;
二、原型链
2.1对象查找属性的机制
当我们通过obj.foo时其实执行的是一个[[get]]操作,那么js 引擎
1.会去此对象的属性上找
2.咩有的话,去内置[[prototype]]属性所指向的对象,即此对象的原型对象上找(通过浏览器实现__proto__指针可以很方便的获取此原型对象);
3.如果还咩有,此对象的原型对象也有内置[[prototype]]属性,我们使用__proto__指针去此对象的原型对象的原型对象上查找
4.就这样一直通过__proto__指针沿着原型对象找,这就是原型链,原型链的顶端是object函数的原型对象,object函数的原型对象的__proto__指针指向null,故找到这里结束;
所以,原型链就是一条对象查找属性时,会不断通过proto指针指向父级原型对象的链子;
注意:
[[get]]操作是沿着原型链查找,那如果是[[set]]操作呢?
发现本对象没有这个属性,直接新建一个属性在本对象;
2.2原型链的顶层原型对象为什么是Object函数的原型对象 ?
首先,
不管哪种方式创建对象,本质上都会把object函数的原型对象赋值给实例对象的内置[[prototype]]属性上;(参考1.2函数原型上,new一个构造函数,就是会把函数原型赋值给对象原型;)
即使第一种字面量形式,我们也看到是这样,可能字面量创建对象形式只是new Objecg()的语法糖;
使用构造函数新建的对象,沿着原型链去找,它的顶层对象也是Object;
其次,
当我们手动修改对象的原型时,比如这样,顶层对象还是Object.prototype;
Object函数是所有函数的父函数,它们的原型对象,都是由Object创建的;
2.3Object.prototype
该object原型对象的__proto__指针就指向null了;
默认的属性和方法的enumable属性时false,所以要使用getOwnProtorypeProtery()方法来获取,这样就能看到toString、valueOf等方法了;
Object函数的原型对象有什么特殊的?
三、继承
为什么要继承?
- 如果父类有子需要的代码,那么子类就不用写了;
- 继承是多态的前提;
3.1继承之父类实例对象的原型赋值给子类原型
js也是一门面向对象的语言,封装继承多态,都具备,那么继承如何实现?
如何实现继承?
既然我们知道了对象查找属性的机制,又知道了什么是原型链,那么我们只需要将父类实例对象的原型赋值给子类原型就可以了。
这样的话,父类原型里是要继承的属性和方法;(第二步)
我们也可以在子类原型里即父类实例对象原型定义各个子类实例对象公共有的方法。(第五步)
为什么不将父类的原型赋值给子类原型?
如果这样的话,子类如果想添加一个本子类特有的属性和方法将只能添加在父类原型上,将导致其他子类竟然也能使用本子类的特有属性和方法,这坑定不对;
如果这样的话,子类实例对公有属性的修改竟然能影响到父类,进而影响到了其他的子类,这坑定不对;
所以使用 父类实例对象的原型赋值给子类原型,这样的话,父类实例对象的原型即子类原型的属性和方法是子类实例的公有属性方法,父类原型的属性和方法是所有子类实例继承到的;
3.2方式简单,但是问题很多
这种方式,确实可以实现继承,不同的子类确实可以继承Person类的running方法,
但 有很多问题,
不同的子类之间,如下:
//1.name是一个不同子类实例都有的属性,完全可以放到父类中,但是没办法放到父类中;因为无法调用父类;
//2.调用了多次的new Person();
function Person(name) { this.name = name; } Person.prototype.running = function () { console.log(this.name + 'running'); }; function Student(name, sId) { this.name = name; this.sId = sId; } function Teacher(name, teacherId) { this.name = name; this.teacherId = teacherId; } const p = new Person();
const p1 = new Person(); Student.prototype = p; Teacher.prototype = p1; Student.prototype.studying = function () { console.log(this.name + 'studying'); }; Teacher.prototype.teaching = function () { console.log(this.name + 'teaching'); }; const s1 = new Student('kobe', 1); const s2 = new Student('james', 2); const t1 = new Teacher('lihua', 1); const t2 = new Teacher('xiaomig', 2); console.log(s1.running()); console.log(s1.teaching()); console.log(s2.running()); console.log(t1.running()); console.log(t2.running());
koberunning
undefined
kobeteaching
undefined
jamesrunning
undefined
lihuarunning
undefined
xiaomigrunning
undefined
四、继承的改造
4.1借用构造函数继承
不同的子类都会有的name属性和age属性解决了,都可以放到person里了;
但是还是new好多次person类,每个person实例都保留了name、age等属性虽然是undefined,但也占用内存;而且
console.log(s1); //Person { name: 'kobe', age: 18, sId: 1 }
时我们也会看到si其实是Student类,但是还是打印的Person类,为什么?
因为打印类型时会去函数原型里找constructor属性, 这个属性的值时构造函数的名字;
4.2寄生组合式继承
4.2.1.先理解什么是对象的原型式继承
对象的原型式继承:
什么是对象的原型式继承?
就是给我传入一个对象,将会作为新创建的对象的隐式原型;
三种方式:
第二种方式是早期没有Object.setPrototypeOf方法和Object.create()方法时使用的;
4.2.2使用
避免了多次new Person实例;
及子类实例的类型也是Student类型了;
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.running = function () { console.log(this.name + '---running'); return 'aaaa' //函数没返回值时,nodejs执行js文件会打印一个undefined; }; function Student(name, age, sId) { Person.call(this, name, age); this.sId = sId; } function Teacher(name, age, teacherId) { Person.call(this, name, age); this.teacherId = teacherId; } //ecma提供的原型式继承函数 // Student.prototype = Object.create(Person.prototype); // Teacher.prototype = Object.create(Person.prototype); //自己写的原型式继承函数 Student.prototype = createObject(Person.prototype); Teacher.prototype = createObject(Person.prototype); Student.prototype.studying = function () { console.log(this.name + 'studying'); };
Teacher.prototype.teaching = function () { console.log(this.name + 'teaching'); }; const s1 = new Student('kobe', 18, 1); const s2 = new Student('james', 19, 2); const t1 = new Teacher('lihua', 38, 1); const t2 = new Teacher('xiaomig', 39, 2);
console.log(s1.running()); //koberunning console.log(s1.studying()); //kobe studying console.log(s1.teaching()); //无法使用,会报错 console.log(s2.running()); //jamesrunning console.log(t1.running()); //lihuarunning console.log(t2.running()); //xiaomigrunning function createObject(targetProto){ function InternalFunc(){}; InternalFunc.prototype = targetProto; const newObj = new InternalFunc(); //newObj.__proto__ 等于 InternalFunc.prototype 等于 targetPrototype; return newObj; }
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.running = function () { console.log(this.name + '---running'); }; function Student(name, age, sId) { Person.call(this, name, age); this.sId = sId; } function Teacher(name, age, teacherId) { Person.call(this, name, age); this.teacherId = teacherId; } //encapsulate inheritPrototype(Student, Person); inheritPrototype(Teacher, Person); Student.prototype.studying = function () { console.log(this.name + 'studying'); }; Teacher.prototype.teaching = function () { console.log(this.name + 'teaching'); }; const s1 = new Student('kobe', 18, 1); const s2 = new Student('james', 19, 2); const t1 = new Teacher('lihua', 38, 1); const t2 = new Teacher('xiaomig', 39, 2); console.log(s1); //Person { name: 'kobe', age: 18, sId: 1 } console.log(s1.running()); //koberunning console.log(s1.studying()); //kobe studying //console.log(s1.teaching()); //无法使用,会报错 console.log(s2.running()); //jamesrunning console.log(t1.running()); //lihuarunning console.log(t2.running()); //xiaomigrunning //functon会作用域提升,放到go里的,并且值是一个内存地址指向fo对象, //所以放到最后面也无所谓,上面的代码也可以正常调用; function inheritPrototype(subFunc, parFunc) { subFunc.prototype = createObject(parFunc.prototype); Object.defineProperty(subFunc.prototype, 'constructor', { configurable: true, enumerable: false, writable: true, value: subFunc }); } function createObject(targetProto) { function InternalFunc() {} InternalFunc.prototype = targetProto; const newObj = new InternalFunc(); //newObj.__proto__ 等于 InternalFunc.prototype 等于 targetPrototype; return newObj; }
这个只是做了下封装;
五、原型继承关系
所有的函数其实都是有函数原型,但是也有对象原型的;
为什么会有对象原型?
因为函数也是对象,为啥函数也是对象?其实function Foo(){} 是 const Foo = new Function()的语法糖;
ecma规范规定,在new一个Functon类的时候,给Foo添加函数原型Foo.prototype ={}, 还有对象原型Foo.__proto__ = Function.prototype;
Foo的
函数原型:显式原型:Foo.prototype = {}
对象原型:隐式原型:Foo.__proto__ 即Foo.[[prototype]];
其实原型链用这个解释也可以,主要突出函数也是有对象原型的这一点;
先看右边的,右边的容易理解;
主要就是说,创建一个实例对象foo时,foo实例的对象原型foo.__proto__ = Foo.prototype;
Foo.prototype是一个对象,既然是一个对象,那么就是由Object类创建出来的,那么也有对象属性然后指向Object类.prototype;
Object类.prototype也是一个对象,但是这个对象的对象属性默认指向null了;
下面的图,是原型链很全的解释,不仅包括了OBject类,还有Function类;
注意,
- Object.prototype.__proto__ = null
- Funtion.__proto__ = Function.prototype, 一般来说,函数的对象原型都是指向Function.prototype, 由于Function类也是一个函数,所以也不例外,也执行自身的函数原型;这是很特殊的一点,Function构造函数的对象原型指向自身函数原型;
- Functon.prototype.__proto__ = Object.prototype, 而Object.prototype最终指向的还是一个null,所以null是最顶层的原型了。