JavaScript 原型链
原型链
1. 如何构成原型链?
2. 原型链上属性的增删改查
3. 谁调用的方法内部this就是谁-原型案例 new
4. 绝大多数对象的最终都会继承自Object.prototype
5. Oject.create(原型)
6. 原型方法上的重写 new
一、如何构成原型链?
什么是原型链?构造函数构造出的对象通过 __proto__ 可以找到原型的,现在我们看看原型里面有什么?
function Person(){ } console.log(Person.prototype); // 打开原型
原型也是对象,打开 Person.prototype 发现原型这个对象里面也有一个__proto__属性,我们知道__proto__是指向原型的,说明原型它还有原型。
既然系统说了,原型可以有原型,那么我们按照规则假设一下,写一大堆原型链成链
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__
再点开是这个__proto__对象,显示出的这一大串东西里有 toStrong 方法
然而 Grand.prototype 里面的__proto__,指向的是 Object.prototype 是所有对象的最终原型
Object.prototype 是所有对象的最终原型,点开之后就会发现里面没有__proto__了,说明它是原型链终端了,和刚才一样就是没有__proto__
再访问 Object.prototype.__proto__,试试有没有__proto__,没有了返回null,它就是终端
访问 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
对象字面量不是工厂里造出来的有原型吗?必须有原型
对象字面量 和 系统提供的构造函数,这两种方法是完全一样的
var obj1 = new Object(); // Object()是系统提供的构建函数(就是一个空对象) console.log(obj1);
为什么说一模一样呢?因为打印出来是一样的
换句话说要写个对象字面量,系统会在内部会来一个 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
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
Object.prototype返回的这一大堆,上面还有constructor指向的是Object(),把构造器给指回去了,对象访问constructor就返回它的构造器
var obj = {}; console.log(obj.constructor); // ƒ Object() { [native code] }
虽然对象obj是通过字面量形式创建的,但是访问它的构造器constructor返回的是 Object() { [native code] }
构造函数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,构造出的对象没有原型
对象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__
能不能人为的给它设置一个__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方法的