Go to comments

JavaScript 原型链

原型链


一、如何构成原型链?

什么是原型链?构造函数构造出的对象通过 __proto__ 可以找到原型的,现在我们看看原型里面有什么?

function Person(){

}

console.log(Person.prototype); // 打开原型

原型也是对象,打开 Person.prototype 发现原型这个对象里面也有一个__proto__属性,我们知道__proto__是指向原型的,说明原型它还有原型。

image.png


既然系统说了,原型可以有原型,那么我们按照规则假设一下,写一大堆原型链成链

Grand.prototype.lastName = "明";
function Grand(){
}
var grand = new Grand();


Father.prototype = grand;
function Father(){
  this.name = "爸爸老明";
}
var father = new Father();


Son.prototype = father;
function Son(){
  this.hobbit = "儿子小明";
}
var son = new Son();

console.log(son.hobbit); // son访问自身的hobbit属性,son自身有这个属性所以打印"儿子小明"

console.log(son.name); // son要访问name属性,son自身没有name属性,按照流程应该找son的__proto__,通过__proto__找到原型是father,原型father身上有name属性,能打印出结果"爸爸老明"

console.log(son.lastName); // 这次son找高级的lastName属性,首先son通过__proto__找到prototype是father,father上面也没有,father也有一个__proto__指向的prototype是grand,完后上grand身上接着找也没有,找Grand的__proto__指向Grand.prototype上才有LastName打印出来"明"


原型上面加一个原型,再加一个原型这样的方法把原型连成链,访问顺序依也照链的顺序,像作用域链一样访问的叫做原型链

原型链的链接点就是__proto__,原型链的访问顺序和作用域链的访问顺序差不多,都是可近的来由近的依次往远的排查,近的有就不访问远的,近的没有就一直往下捋。


现在访问一个过分的,son上有toString方法吗?

我们还没编辑过这个方法,找到grand是已知能看见的头了,但grand是真正的头吗?

Grand.prototype.lastName = "明";
function Grand(){
}
var grand = new Grand();


Father.prototype = grand;
function Father(){
  this.name = "爸爸老明";
}
var father = new Father();


Son.prototype = father;
function Son(){
  this.hobbit = "儿子小明";
}
var son = new Son();


console.log(Grand.prototype);

Grand 还不是真正的头,找到 Grand.prototype上面也有 __proto__

image.png


再点开是这个__proto__对象,显示出的这一大串东西里有 toStrong 方法

image.png


然而 Grand.prototype 里面的__proto__,指向的是  Object.prototype  是所有对象的最终原型


 Object.prototype  是所有对象的最终原型,点开之后就会发现里面没有__proto__了,说明它是原型链终端了,和刚才一样就是没有__proto__

image.png


再访问 Object.prototype.__proto__,试试有没有__proto__,没有了返回null,它就是终端

image.png


访问 son.toString 是能找到的,会把 Object.prototype 上的toString方法返回,因为他就是原型链的终端

Grand.prototype.lastName = "明";
function Grand(){
}
var grand = new Grand();


Father.prototype = grand;
function Father(){
  this.name = "爸爸老明";
}
var father = new Father();


Son.prototype = father;
function Son(){
  this.hobbit = "儿子小明";
}
var son = new Son();


console.log(son.toString); // ƒ toString() { [native code] },访问son.toString是能找到的,会把Object.prototype上的toString返回,因为它就是原型链的终端


二、原型链上属性的增删改查

原型连的增删改查和原型的增删改查基本是一致的

查:查看属性就是可近的来,近的没有就往远的找,一直找到原型链的终端,终端都没有那就是undefined

删:能给原型链上任何一个原型删除它的属性吗?通过它子孙是没法删,但是通过自己可以删。

改:除非本人修改,否则后代没法修改

增:后代也没法增,除非自己增


通过自己这样  delete Father.prototype.Fname;  能删除自己原型的属性,但是通过子孙  Son.prototype.Fname;  删除不了,没有这样的权限

Grand.prototype.lastName = "明";
function Grand(){
  this.Fname = "爷爷明不悔";
}
var grand = new Grand();


Father.prototype = grand;
function Father(){
  this.Fname = "爸爸老明";
}
var father = new Father();


Son.prototype = father;
function Son(){
  this.hobbit = "儿子小明";
}
var son = new Son();


console.log(son.Fname); // son访问Fname属性,打印的是son自己原型father里面的Fname输出"爸爸老明"
delete Son.prototype.Fname; // Son删除自己原型的Fname属性,也就是father里面的Fname属性,删除控制台后返回true
console.log(son.Fname); // 删除father的Fname属性后,son再访问Fname属性,找到的是Grand原型的Fname属性"爷爷明不悔"


delete Son.prototype.Fname; // 再通过son继续删除Fname属性,虽然也返回true
console.log(son.Fname); // 但是在访问son.Fname还是输出"爷爷明不悔",说明Son是子孙不能删除Grand上的属性


delete Father.prototype.Fname; // Father删除自己原型grand上的Fname属性
console.log(son.Fname); // son再一次访问Fname,打印输出undefined

father是Son的原型,Son只能删除自己原型father里面的属性,不能删除grand上面的属性。


看一个修改小特例的,

不能泛泛的说子孙完全不能修改父元级,比如给Father定义一个引用值。

Grand.prototype.lastName = "明";
function Grand(){
}
var grand = new Grand();


Father.prototype = grand;
function Father(){
  this.name = "爸爸老明";
  this.fortune = { // 定义一个引用值fortune
    card1: "visa"
  }
}
var father = new Father();


Son.prototype = father;
function Son(){
  this.hobbit = "儿子小明";
}
var son = new Son();


console.log(son.fortune); // son访问原型里面fortune属性返回{card1: "visa"}

son.fortune = 200; // 现在son要对他爹的fortune属性进行修改,他爹的fortune可以修改吗?

console.log(son); // 修改后son多了一个fortune属性 Son {hobbit: "儿子小明", fortune: 200}

console.log(father.fortune); // 访问他的爹fortune属性没有被修改 {card1: "visa"}


现在  son.fortune.  后面加一个点  son.fortune.card2 = 'master';  操作fortune

Grand.prototype.lastName = "明";
function Grand(){
}
var grand = new Grand();


Father.prototype = 'grand';
function Father(){
  this.name = "小明爸爸";
  this.fortune = {
    card1: "visa"
  }
}
var father = new Father();


Son.prototype = father;
function Son(){
  this.hobbit = "儿子小明";
}
var son = new Son();


son.fortune.card2 = 'master'; // son操作fortune增一个car2

console.log(son); // 访问Son没有fortune属性 {hobbit: "儿子小明"}

console.log(father.fortune); // 访问他爹Father的fortune属性,被修改了增加了一个car2 {card1: "visa", card2: "master"}


这种  son.fortune.card2 = master;  修改,son调用了fortune在fortune的基础上又增加了东西,相当于调用fortune的方法,因为fortune是引用值。

这种意义上的修改是引用值自己的修改,引用值可以自己给自己加属性,不论谁调用,引用值操作的都是自己。


son.fortune 引用值 fortune被取出来了,fortune.name 就是给自己加东西了,

这不算是赋值的修改是一种调用的修改(方法的修改),这种层面的修改是可以修改的,另外一种直接给属性赋值,覆盖性的修改是不行的。

这种修改仅限于引用值,原始值是不能修改的,原始值只能覆盖。


玩一个小游戏,给Father添加一个num属性。

Grand.prototype.lastName = "God";
function Grand(){

}
var grand = new Grand();


Father.prototype = grand;
function Father(){
  this.name = "xuming";
  this.fortune = {
    card : "visa"
  }
  this.num = 100; // 加一个num属性
}
var father = new Father();


Son.prototype = father;
function Son(){
  this.hobbit = "smoke";
}
var son = new Son();


console.log(son.num); // son可以访问到爹的num属性返回100

son.num++; // son.num++ 能实现吗?操作不报错

console.log(father.num); // father查看自己的num属性返回100,改不了,那加加的数那去了?

console.log(son.num); // son访问num返回101,这个过程是son把num取过来在加1,"son.num = son.num + 1"然后再赋值就变son自己的了

console.log(son); // 查看son多了一个属性"num = 101" {hobbit: "smoke", num: 101}

1. son.num先取num过来,

2. 然后再赋值,

3. 赋完值就变成son自己的了 son.num = son.num + 1,所以他爹的没变,son自己的变了


三、谁调用的方法内部this就是谁

涉及一点 this 的知识


对象 Li 调用 sayName() 方法打印的结果是什么?

Li 身上既没有name属性也没有 sayName()方法都是Li继承来的,就原型一个name属性,打印结果是"a"

Person.prototype = {
  name: "a",
  sayName: function(){

    // console.log(name) 
    // 直接写name是错的,因为没有这个变量,
    // 至少要打印谁的name
    // 谁的name?this的name
    console.log(this.name);

  }
}

function Person(){

}

var Li = new Person();

Li.sayName(); // a


小常识:

比如 a.sayName()

1. a调用sayName()方法

2. sayName()方法里面有this,

3. this的指向是,谁调用的sayName()方法this就指向谁(谁调用的方法this就是谁)


下面增加点难度,

构造函数Person自己也有name属性,这次打印什么?

Person.prototype = {
  name: "a",
  sayName: function(){
    console.log(this.name);
  }
}

function Person(){
  this.name = "b"; // 构造函数自己也有name属性
}

var Li = new Person();

Li.sayName(); // 对象Li调用的sayName()方法,this就是对象Li,打印的就是 b


原型调用的sayName方法,打印的是原型自己的 a

Person.prototype = {
  name: "a",
  sayName: function(){
    console.log(this.name);
  }
}

function Person(){
  this.name = "b"; // 构造函数自己也有name属性
}

var Li = new Person();

Person.prototype.sayName(); // 原型调用的sayName方法,打印的是原型自己的 a


有一个 eat 吃方法,只要调用eat方法,heigth属性就加加

Person.prototype = {
  height: 100 // 这个100怎么都不变
}

function Person(){
  this.eat = function(){
    this.height ++;
  }
}

var Li = new Person();

Li.eat(); // Li调用eat方法,每次调用结果是什么?

console.log(Li); // 对象Li上面多了一个height:101属性,这就是上面那个原始值覆只覆盖的还原 Person {height: 101, eat: ƒ}

console.log(Li.__proto__); // Li的原型上height没有变 {height: 100}


ps:

控制台调用方法默认返回值是undefined,因为的没有设置返回值return,默认返回值就是undefined,设置"return 123"就返回123,控制台每次都会把方法执行的return的结果返回来。


增删改查,怎么形成原型域链基本完事了。


四、大多数对象的最终都会继承自Object.prototype

这是  var obj = {}  对象字面量的创建形式,是最简单的创建对象的形式,这个对象有没有原型?

var obj = {};

console.log(obj); // 点击展开是 __proto__: Object

对象字面量不是工厂里造出来的有原型吗?必须有原型

image.png


对象字面量 和 系统提供的构造函数,这两种方法是完全一样的

var obj1 = new Object(); // Object()是系统提供的构建函数(就是一个空对象)

console.log(obj1);

为什么说一模一样呢?因为打印出来是一样的

image.png


换句话说要写个对象字面量,系统会在内部会来一个  new Object() 

var obj={}; // var obj={} --> new Object()


PS:

平时构造对象,能用对象字面量就不用系统提供的构造函数,字面量的写法更简单,公司在开发规范里面数组、对象用必须用字面量的写法,这样写Object()太麻烦了还没有什么用,写属性方法写起来也费劲,还不如直接写到这对象字面量换括号{}里面。


Object.prototype是原型链的终端

 var obj = {}  和  var obj1 = new Object()  是画等号的,那  new Object()  的原型是谁?

1. Object()是构造函数,

2. 原型是 Object.prototype,

3. 而且 Object.prototype 是原型链的终端

var obj1 = new Object();

console.log(obj1.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

obj1的原型是 obj1.__proto__   --->  Object.prototype

image.png


Object() 原型是 Object.prototype,对象字面量的原型就也是 Object.prototype,所以字面量创建的obj对象天生就有 toStrong 方法

var obj = {};

console.log(obj.toString()); // [Object Object]

obj对象的原型是 Object.prototype,toString方法在Object.prototype上,obj调用toString返回[Object Object]


toString方法在Object.prototype上,控制台输入  Object.prototype 

image.png

Object.prototype返回的这一大堆,上面还有constructor指向的是Object(),把构造器给指回去了,对象访问constructor就返回它的构造器

var obj = {};

console.log(obj.constructor); // ƒ Object() { [native code] }

虽然对象obj是通过字面量形式创建的,但是访问它的构造器constructor返回的是 Object() { [native code] }

image.png


构造函数Person有一个默认的  Person.prototype = {} ,默认的 Person.prototype 就是一个对象自面量{},所有对象自变量的原型就应该是 Object.prototype,所以原型链的终端是 Object.prototype 是毋庸置疑的。

// Person.prototype = {} // {} --> Object.prototype

function Person(){

}


五、Object.create(原型)

这个应该放的包装类那节课,可那节课还没法学


Object() 上有一个  create()  方法也能创建对象,系统规定  create( 原型 );  括号里必须写一个原型。


现在创建一个对象,并且原型可以自己指定

var obj = {name: "sunny", age: 123};

var obj1 = Object.create(obj); // obj1是一个对象,它的原型是括号里面的obj

console.log(obj1.name); //  sunny

console.log(obj.name); //  sunny

obj1是一个对象,他的原型是括号里面的obj,所以现在 obj1.name 就是 obj.name,这是一种更加灵活的创建对象的方法。


下面是一个构造函数Person,现在想通过 Object.create() 方法构造出和 new Person() 一样的效果怎么做呢!

Person.prototype.name = "sunny";

function Person () {

}

var obj = Object.create(Person.prototype); // 让构造函数Person的原型归到obj对象,就和new Person()的生成方法差不多了

让构造函数Person的原型归到obj对象,就和 new Person() 的生成方法差不多了(除非在工厂里写些自定义的就不好模拟了)


阿里巴巴面试选择题里其中一项,所有的对象最终都会继承自 Object.prototype 对不对?

目前学习中所接触的对象都继承自 Object.prototype,它是终端绕不开,有没有例外呢?有例外,因为 Object.create() 方法出现了特例。


Object.create() 里面必须填原型,如果不填东西,是不是就构造出没原型的对象?

var obj = Object.create(); // Uncaught TypeError: Object prototype may only be an Object or null: undefined at Function.create (<anonymous>)

不填原型会报错,Object prototype may only be an Object or null,意思是一个对象的原型只能是一个对象 或者 null。


可以填两种,"对象"或者"null",null不是对象但可以填到里面

var obj = Object.create(null);

console.log(obj);

现在构造出的对象obj,点开显示 No properties,构造出的对象没有原型

image.png


对象obj没原型,有toString()方法码?

var obj = Object.create(null);

console.log(obj.toString()); // Uncaught TypeError: obj.toString is not a function

对象obj没有原型,调用toString方法,返回Uncaught TypeError: obj.toString is not a function,意思是没有toString方法。


obj是对象可以有属性

var obj = Object.create(null);

obj.name = 123;

console.log(obj); // {name: 123}

但是点开后就是没有__proto__

image.png


能不能人为的给它设置一个__proto__,没试过我们试一下

var obj = Object.create(null);

obj.__proto__ = {name: "sunny"}; // 人为的给对象加一个原型

console.log(obj); // 有__proto__但显示的颜色不是系统定义的粉色

console.log(obj.name); // 返回undefined,没有继承特性

我们设置的__proto__系统不会去读取,所以原型是隐式的内部属性,我们设置的是不管用的。


现在看,全部的对象的最终都会继承自 Object.prototype 是错的,一定是绝大多数对象的会继承自 Object.prototype。


类型转换的时候,说toString()方法记住两点undefined、null不能调用,现在知道是为什么呢?

数字能调用toString()因为能经过包装类,一层一层往上访问,包装类包装起来是一个对象,对象的最终原型链的终端是 Object.prototype 有 toString()方法。

var num = 123;

num.toString(); // "123"

调用toString方法返回字符串"123"


undefined 是没有包装类的,它就是一个原始值也没有原型,没有原型就不可能有toString()方法,也不是对象也不能经过包装类,null也没有原型也不是对象,所以访问toString()会报错。


只有undefined和null,加上我们构造出来没有原型的对象没有toString()方法

undefined.toString();

null.toString();

下面探究一下toString()


六、原型方法上的重写

探究一下toString()方法,各个变量、各个属性值调用toString()返回的结果是不一样的。


布尔值true返回字符串形式"true"

console.log(true.toString()); // "true"


数字 123.toString() 这么调用是不行的

console.log(123.toString()); // Uncaught SyntaxError: Invalid or unexpected token

报错信息 Uncaught SyntaxError: Invalid or unexpected token


为什么这样 123.toString() 调用报错?

首先识别成浮点型,正常对象来说这个  是调用方法,但是数学计算里这个 点 优先级是最高的,会认为123点后面是数字当成浮点型,浮点型后面加字母肯定是不行的,因为不会优先识别为对象调用的。

所以数字转换成变量,这个点就识别了返回字符串形式的123

var num = 123;

console.log(num.toString());// "123"


对象调用toString()返回的不是字符串形式的花括号{},返回的是[object Object]

var obj = {};

console.log(obj.toString()); // [object Object]

对象调用的toString()方法是Object.prototype上面的toString()方法,因为对象一层父级就到终端了。


而数字调用的toString()方法是谁的?

var num = 123;

console.log(num.toString()); // "123"

num.toString()

1. 数字调用 num.toString(num) -- 要经过包装类包装 --> new Number(num).toString()

2. new Number() 的 toString方法是谁的呢?

    new Number()的原型是 Number.prototype

3. number.prototype 上面就有 toString()方法( number.prototype.toString = function(){} )

4. 然后Number.prototype也有原型( Number.prototype.__proto__ = Object.prototype  )

    原型是Object.prototype,所以这是一个原型链

5. 原型链的意思 Number() 的原型 Number.prototype 上有toString()方法,

    那就不用Object.prototype上的toString()方法,所以Number调用的toString()是自己 重写 过的。


原型上有这个方法,我自己又写了一个和原型上同一个名字不同功能的方法叫做重写。

比如,Object.prototype上面有一个toString()方法,任何继承自它的对象都会用这个toString()方法

Person.prototype = { // Person.prototype是一个对象,这个对象继承自终端Object.prototype 

}

function Person(){

}

var Li = new Person();

console.log(Li.toString()); // [object Object]

toString()是原型链找到终端Object.prototype上的方法,终端上的方法返回的是[object Object]


现在重写toString()方法,不想找到原型链终端,想调用自己写的toSting()方法。

// person.prototype有自己的toString方法就不找终端上的了

Person.prototype = { 
  toString : function(){
    return 'hehe';
  }
}

function Person(){

}

var Li = new Person();

console.log(Li.toString());// "hehe"

这种和原型链上终端里名字一样的方法,但实现不同的功能叫方法的重写(重写就是覆盖)


重写是泛泛的概念,比如系统自带一个 Object.prototype.toString 方法,但是我们覆盖掉系统的这个方法也叫做重写

Object.prototype.toString = function(){ // 覆盖掉系统的方法
  return 'haha';
}

// Person.prototype = { 
//   toString : function(){
//     return 'hehe';
//   }
// }

function Person(){

}

var Li = new Person();

console.log(Li.toString()); // 会执行我们覆盖的方法返回"haha"


方法的 重写 不仅存在我和机器之间,程序自己也重写自己的东西

Object.prototype.toString     有一个toString方法

Number.prototype.toString   重写了

Array.prototype.toString        重写了

Boolean.prototype.toString   重写了

String.prototype.toString       重写了 


所以数字类型 123点toString() 调动的是 Number.prototype.toString()方法,不是终端上的方法

var num = 123;

console.log(num.toString()); // 返回数字"123"


如果数字123调用最终原型上 Object.prototype.toString 的方法,输出的结果不是这样的?

跳过自己的String()方法,直接调用原型上的toString方法

console.log(Object.prototype.toString.call(123)); // [object Number]


布尔值调用终端的toString()方法

console.log(Object.prototype.toString.call(true)); // [object Boolean]

真正顶端的 toString() 方法显示的信息是没什么用的,所以后续的要重写方法。


当然我们也可以任性一点,在原型链上编程重改原型链,重新写系统构造方法Number()上的toString方法

Number.prototype.toString = function(){
  return '因为热爱所以执着';
}

var num = 123;

console.log(num.toString()); // '因为热爱所以执着'


为什么讲toString因为它非常神奇,有一个特别好玩的地方,这个document.write()方法往页面里输出内容,然而并不是真正的输出内容,输出的是内容是调用一个方法之后的结果。


document.write()方法打印一个对象字面量,页面上输出[object Object]

var obj = {};

document.write(obj); // [object Object]


让输出的对象变成自己创建的没有原型的对象,再输出就报错了!

var obj = Object.create(null);

document.write(obj); // Uncaught TypeError: Cannot convert object to primitive value


为什么报错?

因为document.write()往页面输出的时候,其实会隐式的调用toString()方法,把这个对象真实的展示情况反回来去打印。

其实打印的是  document.write( obj.toString() );  的结果,如果一个对象没有原型就不能调用到toString(),怎么来验证是调用toString()方法呢?


还是没有原型的对象,人为的加上一个toString()

var obj = Object.create(null);

obj.toString = function(){
  return '天下我有';
}

document.write(obj); // "天下我有"

明明打印的是obj,出来的是"天下我有",说明一定会调用toString方法的



Leave a comment 0 Comments.

Leave a Reply

换一张