原型链

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。构造函数Personprototype属性,且该属性指向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步。

  1. 获取arguments中的第一个参数,即Constructor构造函数。
  2. 使用Object.create方法来创建一个对象obj,并将obj.__proto__指向Constructor.prototype。此步是实现new的继承原型对象的作用。
  3. 调用构造函数Constructor,并将Constructor构造函数中的this指向obj(此步是实现new有构造函数里面的属性作用),同时将剩下的两个参数传递给Constructor。获取Constructor的返回值result。
  4. 判断result的类型,如果result是复杂数据类型,那么就返回result,这里需要进一步进行判断,如果result是null,null的类型也是object,此时将不会有任何影响,所以是result||obj;如果result是简单数据类型,那么就返回obj。

四.面试题考点总结

  1. 三角关系图
  2. apply,call
  3. prototype的赋值
  4. 对象属性方法、原型属性方法、静态属性方法的获取调用和继承
  5. 构造函数的返回值问题
  6. This指向
  7. Object.create的使用
  8. 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来实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值