在平时使用数组时,看到以下代码,你是否有产生过疑问,数组里的push,pop,shift,unshift,,字符串里 的slice,length方法是哪里来的,我没定义它啊,而且,不应该是对象才能使用''对象名 + . + 属性名''这 种方式来调用对象里的内容吗,为什么字符串,数组,以及函数等等都可以使用这种调用方式呢???
ini
const arr = [1,2,3]
arr.push(4) //为什么我新建的这个数组就可以使用push方法???
const str='hello world'
let num = str.length //这些属性是哪里来的呢???
let obj = {name:''zzh''} //一个真正的对象
console.log(obj.name)
obj.printname =() => {console.log('your name')}
你会发现,很多明明不是对象的数据类型,却透露着种种对象的气息,你是否发现一个字符串str,居然可以像对象obj一样访问他的属性.length,数组str调用方法push()。这究竟是为什么呢,难道说,这些数据类型天生就是对象?
没错,在js中你可以理解为万物皆对象,我接下来将为你解释为何万物皆对象,为了帮助理解,我们需要来聊聊原型与原型链,因为让你感到疑惑的这一切其背后都是'原型'在默默工作着,理解了原型,我们才能真正理解js的面向对象编程
一、创造实例对象 ------------ 实例对象的继承
javascript
function Person(name){
this.name = name
}
Person.prototype.yourname = function (){
console.log(this.name);
}
const person1 = new Person('z')
const person2 = new Person('a')
person1.yourname() // z
person2.yourname() // a
有没有发现一件事,yourname()方法只在Person.prototype中定义了一次,但使用new Person()构造出来的实例对象perosn1,与person2却均可调用,观察下来,明明''person1与person2都只继承了name属性'',可他们为什么都能调用相同的保存在Person.prototype中的yourname方法呢,实例对象是怎么调用这个方法的,这个方法又是存在哪的
二、何为原型,详解prototype,__prototype__ , constructor
1. prototype (函数的显示原型)
函数天生拥有一个属性(prototype),箭头函数除外,可以被函数本身访问到但是,由于不是显示属性,所以你并不能在函数体中观察到,通过在游览器中打印该属性,我们可以看到以下内容:

可以看见,函数的prototype是以一个对象的形式存在的,该对象中包括了:
- 共享的方法与属性,这也是prototype存在的主要的目的,用以存储所有实例共享的方法和属性,这样通过把方法和属性定义在prototype上,可以避免每个实例都创建一份方法的副本,节省内存
javascript
function fn(name,age){
this.name = name
this.age = age
}
fn.prototype.foo = function(){
console.log('hello') //将实例对象需要共享的方法放在prototype上,使得所有实例对象共享一份,避免了每创建一个实例对象便创建一个foo函数,减少了内存的占用
}
const person1 = new fn()
const person2 = new fn()
console.log(person1.foo === person2.foo) //得到true,可证明person1 与person2 是同一个方法
2. constructor 属性(默认存在),每个构造函数的prototype对象都默认有一个constructor属性,指向构造函数本身
javascript
function fn(name,age){
this.name = name
this.age = age
}
console.log(fn.prototype.constructor === fn) //true 验证成功
const person1 = new fn('z',18)
console.log(person1.constructor.name) //name 由于constructor指向构造函数本身,所有实例对象可以通过constructor来访问它的构造函数
- prototype对象本身也有一个__prototype__,这构成了原型链的基础,这就是原型链中继承链的基础,对象类型的数据结构都拥有一个__prototype__,称之为隐式原型,由于
prototype本身也是一个对象类型的数据类型,所以它也拥有一个__prototype__
javascript
function fn(name,age){
this.name = name
this.age = age
}
console.log(fn.prototype.__prototype__ === Object.prototype) //true
这使得通过fn 创建的实例对象可以找到并访问Object方法,即实例对象通过它的隐式原型找到它构造函数的显示原型,再通过它的显示原型的隐式原型找到Object(),也可以通过构造函数它的隐式原型找到Function()函数
2. __prototype__ (对象的隐式原型) 注:在谷歌浏览器中表示为[[prototype]]
每一个对象 (包括函数,数组等所有对象类型)在创建时都会内置一个__prototype__
通过__prototype__ ,我们就可以理解new操作符的秘密了:
const zzh = new Person() 这一行代码创建实例对象zzh时,实际上就做了三件事
- 创建一个空对象{}
- 将空对象内的
__prototype__指向Person_prototype.(使得实例对象可以沿着__prototype__向上寻找它的构造函数) - 将构造函数内的
this绑定到这个新对象上,执行构造函数,并在最后返回该新对象
用一串代码表示new在干的事情
javascript
function Person(a,b,c){
const xxx = {} //创建一个空对象
xxx.__prototype__ = Person.prototype //使Person的显示原型指向新对象的隐式原型
Person.call(zzh,a,b,c) //使Person()的this指向新对象
this.a=a
this.b=b
this.c=c
return xxx //返回新对象
}
const zzh = new Person(a,b,c)
3. constructor(构造函数)
在函数的prototype对象上,默认会有一个construct属性,用于指回函数本身,可使得该构造函数创建的实例对象通过它的__prototype__找回构造它的构造函数,所以construct起到一个标识作用,但是,由于该属性是可以被覆盖掉的,它并不完全可靠
观察下图,总结了prototype,__prototype__,constructor三者的关系,可以更好的理解什么是原型

三、原型链的形成
现在,让我们来聊聊Object上的toString方法是如何继承到构造函数所创建的实例对象上的,为何被 构造函数所创建出的实例对象都可以使用toString方法
让我来详细写出实例对象对toSrting方法的查找过程
- js读取到
toString被调用 - 向实例对象
person1中查找 //发现没找到 - 顺着原型链,从该实例对象
person.__prototype__查找到Person.prototype //toString没有定义在这,所以没找到 - 继续沿着
person.__prototype__.__prototype__寻找 // 此时等价于在Person.prototype.__prototype中寻找,由于Person.prototype也是一个对象,所以它也有隐式原型,且由于Person.prototype是一个用new Object()类似的方法创建的对象,所以通过它的原型所找到的隐式原型指向着Object.prototype(详情见本文二、2.3图所示),此时成功在Object.prototype中找到toString方法,成功调用
ps:原型链的终点为null:Object.prototype.__prototype__,查找到此结束,如果还没查找到方法,则返回undefined
完整的原型链可表示为: person1 --> Person.prototype --> Objectotype --> null
四、总结
此时你也许明白了实例对象里明明没有一些属性和方法却可以调用的原因了,让我们来总结一下所有的知识点
1.js通过原型链实现了继承,每个对象都有一个隐秘的__prototype__链接指向它的原型,形成了一个链式结构,继而使的实现了原型链
2.使用原型链可以解决内存浪费的问题