javascript通过原型链来实现继承,即通过__proto__属性来继承,记住,是通过__proto__属性,不是通过prototype属性。
一.先了解一下构造函数的继承
function Person(name){
this.name = name;
}
Person.prototype.age="11";
var p1 = new Person("张三");
console.log(p1.name);//张三
console.log(p1.age);//11
console.log(typeof p1);
console.log(p1);
console.log(p1.__proto__);
console.log(p1.__proto__==Person.prototype);
P1是Person的一个实例,new操作符做了哪些事:
1.创建一个新对象,为该对象开辟了一块新的内存空间;
2. 将构造函数的作用域赋给新对象(this 就指向了这个新对象) ;
3.执行构造函数中的代码(为这个新对象添加属性),即将构造函数中的属性添加给该对象;
4.返回新对象。
分析以上结果,p1是个对象,p1中有两个属性,一个显示属性name,另外一个是隐式属性__proto__,且p1.__proto__==Person.prototype。
任何对象都有一个隐式属性__proto__。首先,p1会在自身属性里面找name属性,有就取自身属性name的值张三;然后p1在自身属性中找age属性,在自身属性中没有找到,那么会通过__proto__属性找,p1.__proto__指向Person.prototype,即p1会通过Person.prototype去找age属性,最后找到Person.prototype中的age属性值11。此过程就是通过__proto__属性来实现继承。
我们继续来看下Person.prototype里面有哪些东西
function Person(name){
this.name = name;
}
Person.prototype.age="11";
var p1 = new Person("张三");
console.log(p1.__proto__);
console.log(p1.__proto__==Person.prototype);
console.log(p1.__proto__.constructor==Person);
console.log(Person.prototype==p1.__proto__)
分析结果:p1.__proto__指向Person.prototype(原型对象),原型对象里面有个隐式属性constructor, 且原型对象的constructor属性指向Person,所以p1.__proto__.constructor指向Person,由于p1是通过__proto__属性来继承Person.prototype里的属性,那么p1会继承p1.__proto__的constructor属性,所以p1.constructor指向Person。构造函数Person有prototype属性,且该属性指向Person.prototype。以上关系就是著名的三角关系图1:(建议自己画图并理解,画熟练为止,后期三角关系都是建立在此三角关系之上)
接下来,我们接着看Person.prototype里面有什么东西
function Person(name){
this.name = name;
}
Person.prototype.age="11";
var p1 = new Person("张三");
console.log(Person.prototype);
console.log(Person.prototype.__proto__);
console.log(Person.prototype.__proto__==Object.prototype);
console.log(Object.prototype.constructor==Object);
console.log(Person.prototype.__proto__.constructor==Object);
分析结果:Person.prototype是构造函数Person的原型对象,原型对象是对象,所有的对象本质都是通过var o1 = new Object()实现的。同以上基础三角形图1类似,把Person.prototype当做一个整体,即Person.prototype.__proto__指向Object.prototype(Object的原型对象);Object的原型对象的constructor属性指向Object。Person.prototype是通过__proto__属性来继承Object.prototype的属性,所以Person.prototype.constructor指向Object。
在图1的基础上再添加一个三角关系,如图2所示:
接下来,我们来看下Person里面有什么东西:
function Person(name){
this.name = name;
}
Person.prototype.age="11";
var p1 = new Person("张三");
console.log(Person);
console.log(Person.__proto__==Function.prototype);
console.log(Person.__proto__.constructor==Function);
console.log(Person.constructor==Function);
分析结果:Person是构造函数,一切的函数本质是由var f1 = new Function()来实例化的。同基础三角形关系图图1类似,所以Person.__proto__指向Function.prototype(Function的原型对象);Function.prototype的constructor属性指向Function;Person通过__proto__属性继承Function.prototype的constructor属性,那么Person.constructor指向Function。
在图2上再添加一个三角形,如图3所示:
接下来,我们看下Function.prototype里面有什么东西
console.log(Function);
console.log(Function.__proto__==Function.prototype);
console.log(Function.prototype.constructor==Function);
console.log(Function.constructor==Function);
分析结果:Function是个函数,记住,所有的函数本质都是通过var f1 = new Function()实例化出来的,所以Function.__proto__指向Function.prototype,。同以上基础三角形关系图图1类似,Function.prototype的constructor属性指向Fcuntion;Function通过__proto__属性来继承Function.prototype的属性,所以Function继承Function.prototype的constructor属性,即Function.constructor指向Function.
在图3的基础上添加Function的__proto__关系,如图4所示:
接下来,我们看下Function.prototype里面有什么东西
console.log(Function.prototype);
console.log(Function.prototype.__proto__==Object.prototype);
console.log(Object.prototype.__proto__==null);
console.log(Function.prototype.__proto__.__proto__==null);
分析结果:Function.prototype是Function的原型对象,原型对象是对象,把Function.prototype当做一个整体,那么Function.prototype通过__proto__属性继承Object.prototype的属性,即Function.prototype.__proto__指向Object.prototype。Object.prototype是对象的原型对象,对象的原型对象的__proto__指向null,比如我们访问一个对象的age属性,如果自身属性里面没有age属性,那么会一直通过__proto__属性去原型对象里面找,在原型对象里面找不到age属性,会再沿着__proto__属性去上一层原型对象里面去找,直到找到Object.prototype,如果Object.prototype里面找不到age属性,那么将会返回undefined;
在图4的基础上添加Function.prototype和Object.prototype的关系,如图5所示:
此图是由一个个三角形组成的,不难,建议动手写代码,边写边理解边画图,画几遍就掌握原型链了。
二.原型链的基本知识点
知识点1:属性和方法分三种情况:
1.对象属性和对象方法,只有实例对象才有该属性或者方法。
2.原型属性和原型方法,实例对象可通过__proto__属性来继承原型属性和原型方法。
3.静态属性和静态方法,只有构造函数才有该属性和方法。
function Person(age) {
//对象属性 只有实例对象才有该属性
this.age = age;
//对象方法 只有实例对象才有该方法
this.say = function () {
console.log('Hi,my name is' + this.name);
}
}
//原型属性
Person.prototype.gender = "male";
//原型方法
Person.prototype.sleep = function () {
console.log(this.name + 'is sleep');
}
//静态属性 只有构造函数才有该属性
Person.hobby = "code";
//静态方法 只有构造函数才有该方法
Person.eat = function () {
console.log(this.name + 'is eat');
}
var p1 = new Person("张三");
console.log(p1);
console.log(p1.age);
console.log(Person.age);
p1.say();
Person.say();
分析结果:p1是Person的实例化对象,p1中有对象属性age和对象方法say,其实例化时构造函数中的this指向p1。但是只有实例化对象才有对象属性和对象方法,所以Person.age是undefined,执行Person.say()会报错。
修改代码如下:
function Person(age) {
//对象属性 只有实例对象才有该属性
this.age = age;
//对象方法 只有实例对象才有该方法
this.say = function () {
console.log('Hi,my name is' + this.name);
}
}
//原型属性
Person.prototype.gender = "male";
//原型方法
Person.prototype.sleep = function () {
console.log(this.name + 'is sleep');
}
//静态属性 只有构造函数才有该属性
Person.hobby = "code";
//静态方法 只有构造函数才有该方法
Person.eat = function () {
console.log(this.name + 'is eat');
}
var p1 = new Person("张三");
console.log(p1.gender);
console.log(Person.gender);
console.log(Person.__proto__==Function.prototype);
console.log(Person.__proto__.__proto__==Object.prototype);
console.log(Person.__proto__.__proto__.__proto_==null)
分析结果:p1是person的实例化对象,实例化的时候,构造函数中的this指向p1,那么p1有age属性和say方法,但是p1中没有gender属性,那么p1通过__proto__属性来继承Person.prototype的属性,在Person.prototype里找到了gender属性,所以p1.gender的值是male。再看Person.gender,Person是构造函数,函数都是通过var f1 = new Function()实例化出来的,Person中没有gender属性,那么Person通过__proto__继承Function.prototype的属性,在Function.prototype没有gender属性,那么再向上一级原型对象中查找,将Person.__proto__看做一个整体,该整体是一个对象,那么Person.__proto__.__proto__指向的是Object.prototype,在Object.prototype中没有gender属性,再向上一级原型对象中查找,Person.__proto__.__proto__.__proto__指向null,所以返回undefined;
再来看下原型方法:
function Person(age) {
//对象属性 只有实例对象才有该属性
this.age = age;
//对象方法 只有实例对象才有该方法
this.say = function () {
console.log('Hi,my name is' + this.name);
}
}
//原型属性
Person.prototype.gender = "male";
//原型方法
Person.prototype.sleep = function () {
console.log('I am sleeping');
}
//静态属性 只有构造函数才有该属性
Person.hobby = "code";
//静态方法 只有构造函数才有该方法
Person.eat = function () {
console.log(this.name + 'is eat');
}
var p1 = new Person("张三");
p1.sleep();
Person.sleep();
分析结果:p1是Person的实例化对象,实例化的时候,构造函数中的this指向p1,p1中没有sleep方法,p1通过__proto__属性继承Person.prototype的属性,在Person.prototype中找到了sleep方法,执行,打印出I am sleep;再看Person.sleep,跟前面代码类似,最后找到Object.protype.__proto__,指向null,所以此处报错。
知识点2;构造函数里面有返回值。如果是返回简单数据类型则无影响;如果是返回复杂数据类型,那么实例只会有返回的对象的属性,实例没有构造函数和原型对象上的属性。
function Person(name) {
this.name = name;
return 1;
}
Person.prototype.sex="male";
Person.prototype.say=function(){
console.log('hi')
};
var p1 = new Person('张三');
console.log(p1.name);
console.log(p1.sex);
p1.say();
分析结果:构造函数返回简单数据类型1,无影响,所以实例对象p1有构造函数的属性,也继承了原型对象上的属性。
function Person(name) {
this.name = name;
return {
age:22
};
}
Person.prototype.sex="male";
Person.prototype.say=function(){
console.log('hi')
};
var p1 = new Person('张三');
console.log(p1.name);
console.log(p1.sex);
console.log(p1.age);
console.log(p1.sex);
p1.say();
分析结果:构造函数里面返回的是复杂数据类型,测试实例对象p1只有返回的对象的属性。
知识点3:Person.prototype的赋值。需要注意赋值的位置和是否是整体赋值。
function Person(name){
this.name = name;
this.say = function(){
console.log("hi");
}
}
Person.prototype={age:"22"};
var p1 = new Person("张三");
console.log(p1.name);
console.log(p1.age);
p1.say();
分析结果:以上是常规代码。修改代码,再看下:
function Person(name){
this.name = name;
this.say = function(){
console.log("hi");
}
}
var p1 = new Person("张三");
Person.prototype={age:"22"};
console.log(p1.name);
console.log(p1.age);
p1.say();
分析结果:js代码是从上到下执行的,在p1实例化的时候,p1.__proto__默认指向Person.prototype(为了比较,这个Person.prototype的引用地址记作address1,address1指向此时的Person.prototype),再接着执行下面代码Person.prototype={age:"22"}(这个Person.prototype的引用地址记作address2,address2指向此时的Person.prototype));进行整体赋值,即在p1实例化前后,address1和address2是不一样的,但是从始至终,p1.__proto默认指向的Person.prototype的引用地址还是address1。
function Person(name){
this.name = name;
this.say = function(){
console.log("hi");
}
}
var p1 = new Person("张三");
Person.prototype.age=22;
console.log(p1.name);
console.log(p1.age);
p1.say();
分析结果:p1实例化的时候,p1__proto__默认指向Person.prototype(这个Person.prototype的引用地址记作address1),执行Person.prototype.age=22;并没有改变address1,而是改变address1指向的对象Person.prototype的内部属性。
还有一种情况,在构造函数里面写prototype,这种情况与以上的道理一样。
知识点4:通过apply和call实现继承。
需求:有两个构造函数,Person已经定义完毕,另外一个构造函数Dog,要求Dog也有Person构造函数的属性和Person原型链上的属性。
function Person(name){
this.name = name;
this.say = function(){
console.log("hi");
}
}
Person.prototype.hobby="sleep";
Person.prototype.eat=function(){
console.log('eat');
}
function Dog(name,age){
Person.call(this,name);
this.age=age;
}
var d1 = new Dog('旺财',2);
console.log(d1.name);
console.log(d1.age);
console.log(d1.hobby);
d1.say();
d1.eat();
分析结果:在Dog构造函数里面,Person.call(this,,name),即调用Person构造函数,并将Person里面的this指向Dog构造函数,由此来实现了Dog继续Person构造函数里面的属性和方法。但是d1.__proto__指向Dog.prototype,此时,d1并没有继承Person.prototype的属性和方法。问题:怎么才能让d1继承Person.prototype的属性?看下面代码:
function Person(name){
this.name = name;
this.say = function(){
console.log("hi");
}
}
Person.prototype.hobby="sleep";
Person.prototype.eat=function(){
console.log('eat');
}
function Dog(name,age){
Person.call(this,name);
this.age=age;
}
Dog.prototype=Person.prototype;//重点
var d1 = new Dog('旺财',2);
console.log(d1.name);
console.log(d1.age);
console.log(d1.hobby);
d1.say();
d1.eat();
分析结果:将Dog.prototype指向Person.prototype;此时,d1__proto__==Dog.prototype;自然,d1通过__prototo__属性找到Person.prototype,实现了继承。注意:Dog.prototype=Person.prototype与Dog.prototype=new Person()的功能是一样的,都能实现继承Person.prototype的属性;但是使用Dog.prototype=new Person()也会让d1里面有Person构造函数里面的属性,且会让所有的Dog的实例对象的Dog.prototype指向同一个地址。
三.Js的new关键字底层原理
以下代码是不用new关键字,来实现new的功能。
function Person(name) {
this.name = name;
this.say = function () {
console.log("hi");
};
}
Person.prototype.hobby = "sleep";
Person.prototype.eat = function () {
console.log('eat');
};
//实现new
function CreateNew() {
//[].shift方法是截取并返回数组中的第一个参数 获取Person构造函数
//调用[]的shift方法 并将此方法中的this指向arguments
Constructor = [].shift.call(arguments);
//将obj的原型对象指向Constructor.prototype
var obj = Object.create(Constructor.prototype);
//obj来调用执行构造函数 并将arguments传入到构造函数中
var result = Constructor.apply(obj, arguments);
//将Person的返回值进行判断
return typeof result == "object" ? result||obj : obj;
}
var p1 = CreateNew(Person, '12', '34');
console.log(p1.name);
console.log(p1.hobby);
p1.say();
p1.eat();
分析结果:主要是理解原型链。分为4步。
- 获取arguments中的第一个参数,即Constructor构造函数。
- 使用Object.create方法来创建一个对象obj,并将obj.__proto__指向Constructor.prototype。此步是实现new的继承原型对象的作用。
- 调用构造函数Constructor,并将Constructor构造函数中的this指向obj(此步是实现new有构造函数里面的属性作用),同时将剩下的两个参数传递给Constructor。获取Constructor的返回值result。
- 判断result的类型,如果result是复杂数据类型,那么就返回result,这里需要进一步进行判断,如果result是null,null的类型也是object,此时将不会有任何影响,所以是result||obj;如果result是简单数据类型,那么就返回obj。
四.面试题考点总结
- 三角关系图
- apply,call
- prototype的赋值
- 对象属性方法、原型属性方法、静态属性方法的获取调用和继承
- 构造函数的返回值问题
- This指向
- Object.create的使用
- Object.defineProperty的使用
五.面试题解析:
面试题1:如何将对象obj1={0:'a',1:'b',length:2}的属性值变为数组(对象的索引是数字,依次排序,切该对象有length属性)
var obj1={0:'a',1:'b',length:2};
var arr1 = Array.prototype.slice.call(obj1,0);
console.log(arr1);
console.log(obj1);
分析结果:obj1调用Array.prototype.slice方法,并将此方法中的this指向obj1,同时传一个参数0到slice中。Array.prototype.slice 与[].slice具有同样的功能。[].slice()方法是用来截取数组,不修改原来的数组,会返回一个截取的新数组,第一个参数是截取的下标开始位置,第二个参数是截取的下标的结束位置(不包含)。Array.prototype.slice.call能将具有length属性的对象 转成数组,很好理解,var arr1 = [‘a’,’b’].slice(0,1);//[‘a’];我们可以将[‘a’,’b’]数组看成一个对象{0:’a’,1:’b’,length:2},需要注意的是,对象的索引是从0开始,并依次排序。
将上面代码修改如下:
var obj1={1:'a',2:'b',length:2};
var arr1 = [].slice.call(obj1,0);
console.log(arr1);
console.log(obj1);
分析结果:使用slice方法的时候,从对象的索引值为0开始。
面试题2:综合题
function Parent() {
this.a = 1;
this.b = [1, 2, this.a];
this.c = { demo: 5 };
this.show = function () {
console.log(this.a, this.b, this.c.demo);
}
}
Parent.prototype.eat = "eat";
function Child() {
this.a = 2;
this.change = function () {
this.b.push(this.a);
this.a = this.b.length;
this.c.demo = this.a++;
}
}
//Child.prototype指向Parent.prototype实例对象 并且Parent实例对象new Parent有构造函数Person的属性和方法 同时也会继承Person.prototype的属性和方法
//注意此时所有的Child的实例对象的__proto__属性都指向new Parent,即指向同一个地址
Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
//1.show方法中的this指向parent //打印1,[1,2,1],5
parent.show();
//2.通过__proto__找到new Parent,show中的this指向child1 //打印11 [1,2,1],5
child1.show();
//3.通过__proto__找到new Parent,show中的this指向child2 //打印12 [1,2,1] 5
child2.show();
//4.在构造函数中找到change,change中的this指向child1,change中找不到b和c属性,去new Parent中找,this.b.push(this.a)改变了new Parent中的b=>[1,2,1,11],
//child1中的a=>4,new Parent中的this.c.demo = 4,child1中的a=>5 //只执行不打印
child1.change();
//5.在构造函数中找到change,change中的this指向child2,change中找不到b和c属性,去new Parent中找,this.b.push(this.a)改变了new Parent中的b=>[1,2,1,11,12],
//child2中的a=>5,new parent中的c.demo=>5;child2中的a=>6 //只执行不打印
child2.change();
//6.show方法中的this指向parent //打印1,[1,2,1],5
parent.show();
//7.show方法中的this指向child1,由4可知,child1.a=>5;由4、5可知new Parent中的b=>[1,2,1,11,12],new Parent中的c.demo=>5 //打印5,[1,2,1,11,12],5
child1.show();
//8.show方法中的this指向child2,由5可知,child2中的a=>6;由4、5可知new Parent中的b=>[1,2,1,11,12],new Parent中的c.demo=>5 //打印6,[1,2,1,11,12],5
child2.show();
面试题3:Object.create()和Object.defineProperty()的使用
let proto = {
get name() {
return "张三";
}
}
let obj = Object.create(proto);
//通过赋值 影响
obj.name= "李四";
console.log(obj.name);
//通过定义来操作
Object.defineProperty(obj, "name", {
value: "王五"
});
console.log(obj.name);
console.log(proto.name);
分析结果:在原型链上,如果原型链上右同名的属性,比如name属性,那么会阻止赋值操作,但是不会阻止定义操作,所以可以使用defineProperty。建议大家了解下Object.defineProperty(),vue双向数据绑定也是通过Object.defineProperty来实现的。