从底层原理一步步剖析JS的原型和继承【40个疑问解答】

本文从底层原理一步步剖析JS的原型和继承,学习了这些,我觉得自己对这方面的理解的功底明显加厚,之前也总结过几篇相关文章,但是这篇是方便平时使用的理解。也更系统的一步步递进知识点,形成知识体系。懂得这些思想,对于写代码架构有帮助。

没有原型的对象是存在的

Object.create(null, {name:'12'})
复制代码

原型对象与对象方法优先级

let obj = {
    render(){
        console.log(1)

    }
}
obj.__proto__.render = function(){
    console.log(2)
}
obj.render() // 1
复制代码

优先对象方法

函数拥有多个长辈(prototype和_proto__)

函数作为对象使用,调用__proto__上的函数

函数实例化对象,调用prototype上的函数

function User(){}
User.__proto__.view = function(){
    console.log('view')
}
User.view();  //view

User.prototype.show = function(){
    console.log('show')
}
let user = new User();
user.show() // show

console.log(User.prototype == user.__proto__) //true
复制代码

把构造函数作为对象调用,使用__proto__,服务于它自己。prototype通过new出来无数个对象的时候使用

原型关系详解与属性继承实例

let obj = new Object()
Object.prototype.show = function(){
    console.log('show')
}

obj.show()  //show

function User(){}
let user = new User();

//Object.prototype的上级是null
console.log(Object.prototype.__proto__)  //null

//User.prototype和`User.__proto__`也是对象,这两个对象一定也有父级,分别取__proto__,但是这两个没有prototype  
console.log(User.prototype.__proto__ == Object.prototype) //true
console.log(User.__proto__.__proto__ == Object.prototype) //true

// User.prototype和(User.__proto__的父级是同一个
console.log(User.prototype.__proto__ == User.__proto__.__proto__)  //true

//函数作为对象使用,调用__proto__上的函数;函数实例化对象,调用prototype上的函数,本级没找到,往上一级查找
user.show()  //show
User.show() //show
复制代码

User包含prototype和__proto__父级,
prototype和__proto__也是个对象,这个的对象一定也有父级
User.prototype和User.__proto__对象的__proto__都指向Object.prototype

系统构造函数的原型体现

let obj = {} //由构造函数Object创建的实例

let arr = [] //由 new Array()构建的

let str = '' //由 new String()构建的

let reg = /a/i;  // 由 new RegExp()构建的

还有 Boolean  Number等 同理

所以
console.log(obj.__proto__ == Object.prototype) //true
console.log(reg.__proto__ == RegExp.prototype) //true
其他也一样
复制代码

平常使用的一些定义也是由构造函数创建的

自定义对象的原型设置 (setPropertyOf)

let son = {name:'son'};
let parent = {name:'parent', show(){
    console.log(this.name)
}}

Object.setPrototypeOf(son, parent); //设置原型,这样son没有,parent有,son就可以使用

console.log(son) //得到__proto__是parent的
console.log(son.show()) //son   调用parent的方法,但是此处例子this是执行son的,谁调用,this指向谁

console.log(Object.getPrototypeOf(son));  //查看原型
复制代码

设置原型:Object.setPrototypeOf(son, parent); //记住 son在前,parent在后

查看原型:Object.getPrototypeOf(son);

原型中的contructor引用

function User(name){
    this.name = name
}
User.prtotype.show = function(){}
复制代码

原型(prototype)就是个对象,只要是对象就会有原型(Object.prototype),所以

console.log(User.prototype.__proto__ == Object.prototype) //true
复制代码

通过原型找到构造函数

console.log(User.prototype.constructor == User) //true
复制代码

所以可以用User.prototype.constructor 创建实例对象

User.prtotype = {
    constructor:User  
    show(){}
}

如果这样添加show(),会丢失constructor,必须加上以下才不会报错,
因为这改变了prtotype,定义成了新对象

let lisi = new User.prototype.constructor();
lisi.show()
复制代码

User.prototype.constructor的好处:通过原型对象(prototype)找到构造函数来生成无数个新对象

User.prtotype可以定义无数个方法或者属性。

把构造函数作为对象调用,使用__proto__,服务于它自己。prototype通过new出来无数个对象的时候使用,所以prototype功力更强,因为可以加无数个方法和属性。

给我一个对象再生成一个新的对象

通过实例对象可以原型,通过原型的constructor获得构造函数,通过构造函数创建实例对象,所以给我一个对象,再生成一个新的对象。

function User(name){
    this.name = name
}
let user = new User('hannie')
function createByObject(obj,...arg){
    const constructor = Object.getPrototypeOf(obj).constructor
    return new constructor(...arg)
}
let lisi = createByObject(user,'hhy');
复制代码

以上通过user创建lisi对象

总结一下原型链

let arr = []
console.log(arr.__proto__ )  //这个是非标准找原型方法
 Object.getPrototypeOf(arr) //这个是标准找原型方法
 一个原型,再找另一个原型,这叫做原型链
console.log(arr.__proto__.__proto__ == Object.prototype ) // 往上找
console.log( Object.prototype.__proto__ ) //再往上找不到了
复制代码

一个原型,再找另一个原型,这叫做原型链。

父级会影响下一级。其实原型链也是个继承的过程,把函数写到父级中,子级可以用。

原型链检测之instanceof

function A(){}
function B(){}
function C(){}
let c = new C();
B.prototype = c;
let b = new B();
A.prototype = b
let a = new A()
console.log(a instanceof A) //true
console.log(a instanceof C) //true
console.log(a instanceof B) //true
console.log(b instanceof A) //false
复制代码

prototypeObject.isPrototypeOf(object)原型检测

isPrototypeOf() 是 Object 的原型方法(也称实例方法),它定义在 Object.prototype 对象之上,所有 Object 的实例对象都会继承 isPrototypeOf() 方法。

isPrototypeOf() 方法用来检测一个对象是否存在于另一个对象的原型链中,如果存在就返回 true,否则就返回 false。

复制代码

c.biancheng.net/view/5801.h…

in与hasOwnProperty的属性检测差异

in不仅会检测当前对象,还会检查原型链上的对象

hasOwnProperty仅仅会检测当前对象

let a = {url: '111'}
let b = {name: 'hannie'}
Object.setPrototypeOf(a,b)
console.log('name' in a) // true
console.log(a.hasOwnProperty('name')) // false
复制代码

使用call或apply借用原型链

max不传参

let obj = {
    data: [1,2,3,34,5,7]
}
Object.setPrototypeOf(obj, {
    max(){
        return this.data.sort((a,b)=>b-a)[0]
    }
})
console.log(obj.max())

let hannie = {
    lessons: {js: 87, php: 63, node:99, linux:88},
    get data() {
        return Object.values(this.lessons);
    }
}
console.log(obj.max.apply(hannie))
复制代码

max传参

let obj = {
    data: [1,2,3,34,5,7]
}
Object.setPrototypeOf(obj, {
    max(data){
        return data.sort((a,b)=>b-a)[0]
    }
})
console.log(obj.max(obj.data))

let hannie = {
    lessons: {js: 87, php: 63, node:99, linux:88}
}
console.log(obj.max.call(null,Object.values(hannie.lessons);))
复制代码

以上是借用call和apply调用原型链上得方法max

优化方法借用

求最大值可以有Math.max,所以以上方式可以优化

let obj = {
    data: [1,2,3,34,5,7]
}
console.log(Math.max.apply(null, obj.data))

let hannie = {
    lessons: {js: 87, php: 63, node:99, linux:88}
}
console.log(Math.max.apply(null,Object.values(hannie.lessons);))
复制代码

总之,如果自己没有此方法,可以求助其他家的方法

DOM节点借用Array原型方法,变成真实数组

let btns = document.querySelectorAll('button')
btns = Array.prototype.filter.call(btns, item => {
//或者 btns = [].filter.call(btns, item => {
    return item.hasAttribute('calss')
})
复制代码

类似数组调用Array.prototype的方法变成真实数组

合理的构造函数方法声明

function User(name){
    this.name = name;
}
User.prototype.show = function(){
    console.log(this.name)
}
User.prototype.call = function(){
   
}
let lisi = new User('lisi')
let xz = new User('xz')
lisi.show();
xz.show();
复制代码
function User(name){
    this.name = name;
}
服务于通过构造函数创建实例对象
User.prototype = {
    constructor: User //这个一定要写
    show(){
        console.log(this.name)
    },
    call(){
    }
} 
let lisi = new User('lisi')
let xz = new User('xz')
lisi.show();
xz.show();
复制代码

最好把show方法放在原型上复用,而不是在函数中定义,这样避免造成额外的内存开销

User.prototype对象中创建方法的使用场景:服务于通过构造函数创建实例对象

this和原型没有关系的

let obj = {
    name: 'xiao'
}
let User = {
    name: 'hhy',
    show() {
        console.log(this.name)
    }
}
Object.setPrototypeOf(obj, User)
obj.show(); // xiao
复制代码

根据以上实例,this和原型是没有关系

不要滥用原型

以上发现原型功能很大, 但是不能滥用,比如

Object.prototype.hide = function(){
    this.style.display = 'none'
}
复制代码

this始终指向调用的对象
js会很多用第三方库,如果都这么定义,会覆盖代码,造成代码的互相偶尔,从而导致代码不稳定不健壮,强烈不建议在系统(原生)原型上追加方法

Object.create与__proto__设置对象的原型

__proto__除了设置,还可以获取,但是不是官方的

Object.create只能设置,不能获取

let User = {
    name: 'hhy',
    show() {
        console.log(this.name)
    }
}
let obj = Object.create(User, {
    name: {
        value: 'hannie'
    }
})
obj.show()

let xz = {name: 'xz'};
xz.__proto__ = User  //设置
console.log(xz.__proto__)  //获取
复制代码

使用setPropertyOf代替__proto__设置对象的原型

__proto__不是官方的,setPropertyOf是官方的

let xz = {name: 'xz'};
Object.setPropertyOf(xz,User)  //设置
console.log( Object.getPrototypeOf(xz))  //获取
复制代码

__proto__原来是属性访问器

智能判断,是对象就执行,不是对象不执行

let obj = {name: 'hannie'};
obj.__proto__ = {
    show(){
        console.log(this.name)
    }
}
obj.__proto__ = 99; //不是对象不执行
obj.show() // hannie
复制代码

怎么做到的,其原理?

let obj = {
    action: {},
    get proto(){
        return this.action;
    }
    set proto(obj){
        if(obj instanceof(Object)){
            this.action = obj;
        }
    }
}
obj.proto = 99; //不是对象不执行
复制代码

可以输出代码看__proto__对象,其中包含get和set

get set这不是严格意义的属性,是getter和setter,会对设置的值自动判断

但是想设置呢,怎么操作? 只要让原型为null
let obj = Object.create(null) ,否则必须是对象

let obj = Object.create(null)
obj.__proto__ = 99; //不是对象不执行
console.log(obj.__proto__)   // 99
复制代码

改变构造函数原型并不是继承

继承是原型的继承,而不是改变构造函数的原型

Son.prototype = Parent.prototype //这不是继承
复制代码

继承是原型的继承

继承会保留本身的方法

方法一:改变原来原型对象的原型
Son.prototype.__proto__ = Parent.prototype 
复制代码

新建的方法前后都可以

方法二:新建原型对象
Son.prototype = Object.create(Parent.prototype)   //会改变构造函数,应该加上构造函数
复制代码

新建的方法必须放在下面

两种方法的比较?

function User(){}
User.prototype.name = function() {
    console.log('user.name')
}

function Admin(){}

Admin.prototype.__proto__ = User.prototype
Admin.prototype.role = function() {
    console.log('admin.role')
}


/*  继承写在方法下面也没关系,依旧是输出admin.role
Admin.prototype.role = function() {
    console.log('admin.role')
}
Admin.prototype.__proto__ = User.prototype
*/

/* 
//使用Object.create(User.prototype) 定义继承,Admin.prototype.role写在继承上面,找不到role(),必须写在继承下面
Admin.prototype.role = function() {
    console.log('admin.role')
}
Admin.prototype = Object.create(User.prototype) 
*/

//这是对的
Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {
    console.log('admin.role')
}


let a = new Admin();
a.role() //admin.role

function Member(){}
Member.prototype.__proto__ = User.prototype
Member.prototype.role = function() {
    console.log('member.role')
}
复制代码

继承对新增对象的影响,注意Object.create和Son.prototype.__proto__两者实现继承的区别

function Admin(){}
let a = new Admin();  // 立刻实例化,还是指向旧的原型对象
a.role()  //此处报错:找不到role

//创建新的原型对象
Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {
    console.log('admin.role')
}

a.role()  //在继承之前实例化admin,此处也报错:找不到role
复制代码
function Admin(){}
let a = new Admin();  // 立刻实例化,还是指向旧的原型对象
a.role()  //此处报错:找不到role

//改变原来原型对象的原型
Admin.prototype.__proto__ = User.prototype
Admin.prototype.role = function() {
    console.log('admin.role')
}

a.role()  //此处不报错,  admin.role
复制代码

继承对constructor属性的影响(Object.create会丢失constructor)

Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {
    console.log('admin.role')
}
console.dir(Admin.prototype.constructor)  //User  这是因为Admin本身没有,但是往父级找有

//所以需要加这句
Admin.prototype.constructor = Admin

复制代码

禁止constructor被遍历(Son.prototype.constructor = Son这样写会使得constructor可遍历,也就是for in能遍历出来)

其实constructor没必要遍历。

直接Son.prototype.constructor = Son 这么写,用for in循环,constructor会被遍历,所以这样定义constructor

Object.defineProperty(Son.prototype, 'constructor', {
    value: Son,
    enumerable:false
})
复制代码

Object.create和Son.prototype.__proto__两者实现继承的区别

  1. 原理:Object.create是创建新的原型对象;Son.prototype.__proto__是改变原来原型对象的原型

  2. 定义原型方法的位置的区别

  3. 先实例化导致的后果

  4. Object.create会丢失constructor

  5. Object.create中写Son.prototype.constructor = Son会使得constructor可遍历

例子 看前4部分,便于理解

发现这么多问题,怎么合理实现继承呢?继续看下面,比如原型工厂封装继承等

备注:以下实例中为了快些,就还是用Son.prototype.constructor = Son设置构造函数

子级方法重写与访问父级属性

父类方法不够用,可以在子类重写同样的方法,父子存在相同方法,会调用子类的方法

父类的方法协助子类完成方法调用,可以使用Parent.prototype.site()

function Parent(){}
Parent.prototype.show = function(){console.log('parent name')}
Parent.prototype.site = function(){return 'hannie'}
function Son(){}
Son.prototype = Object.create(Parent.prototype) 
Son.prototype.constructor = Son
Son.prototype.show = function(){
    console.log(Parent.prototype.site() + 'son name') //hannie son name   父类方法协助子类,这么使用
}

let son = new Son()
son.show()
复制代码

面向对象的多态(使用原型的继承实现)

不同的形态响应出不同的结果

function User(){}
User.prototype.show = function() {
    console.log('user.name')
}

function Admin(){}
Admin.prototype = Object.create(User.prototype) 
Admin.prototype.constructor = Admin
Admin.prototype.description = function(){
    return '管理员在此'
}


function Member(){}
Member.prototype = Object.create(User.prototype) 
Member.prototype.constructor = Member
Member.prototype.description = function(){
    return '我是会员'
}

for(const obj of [new Admin(),new Member()]){
    obj.show()  //都会调用自身的方法,响应不同的结果
}
复制代码

使用父类构造函数初始属性(Parent.apply(this.args))

不用在每个构造函数中写,统一使用父级中的

function User(name, age){
    this.name = name;
    this.age = age;
}

User.prototype.show = function() {
    console.log(this.name, this.age)
}

function Admin(name, age){
   // User(name. age)  //这样写this指向window
   User.call(this,name, age)
}

//如果参数多,可以用apply传参
/*
   function Admin(...args){
       User.apply(this, args)
    } 
*/

Admin.prototype = Object.create(User.prototype) 
Admin.prototype.constructor = Admin
let hannie = new Admin('hannie', 18)
hannie.show()


function Admin1(name, age){
   User.call(this,name, age)
}
Admin1.prototype = Object.create(User.prototype) 
Admin1.prototype.constructor = Admin1
let hannie1 = new Admin1('hannie', 18)
hannie1.show()

复制代码

这样不用在买个实例中申明类似于show的函数。

使用原型工厂封装继承

以上实例明显看出步骤多,重复工作多,所以进行封装

//封装继承通用方式
function extend(Sub,Sup){
    Sub.prototype = Object.create(Sup.prototype)
    Object.defineProperty(Sub.prototype, 'constructor', {
        value: Sub,
        enumerable:false
    }) 
}
function Admin(...args){
     User.apply(this, args)
 } 
    
extend(Admin, User) // 调用封装的函数
let hannie = new Admin('hannie', 18)
hannie.show()
复制代码

对象工厂派生对象并实现继承

通过工厂不断产生对象

//对象工厂
function admin(name, age){
    const instance = Object.create(User.prototype);
    User.call(instance, name, age)
    //可以添加函数
    User.prototype.role = function() {
        console.log('role')
    }
    return instance  
} 
使用的时候不用new,直接创建对象
let hannie = admin('hannie', 19)
hannie.show()
复制代码

多继承造成的困扰

JS语言没用多继承

需要一个个往上继承,导致很混乱,也导致代码量增加

function Request(){}
Request.prototype.ajax = function() {
    console.log('请求后台')
}

extend(User, Request)

function Credit(){}
Credit.prototype.total = function() {
    console.log('积分统计')
}
extend(User, Credit)

function Admin(...args){
     User.apply(this, args)
 } 
    
extend(Admin, User) // 调用封装的函数

let hannie = new Admin('hannie', 18)
hannie.show()
复制代码

暴露这个问题,下面给出解决方案

使用mixin实现多继承

定义一些功能的定向,当使用的时候,直接合并到原型中

// mixin混合的方式
const Credit = {
    total(){
        console.log('积分统计')
    }
}
const Request = {
    ajax(){
        console.log('请求后台')
    }
}

function Admin(...args){
     User.apply(this, args)
}     
extend(Admin, User) 

Admin.prototype = Object.assign(Admin.prototype, Request, Credit ) // 这是关键
let hannie = new Admin('hannie', 18)
hannie.show();  // hannie 19
hannie.ajax();  // 积分统计
hannie.total(); // 请求后台
复制代码

mixin的内部继承与super关键字

super是当前类的原型

是为了合并到类型

const Request = {
    ajax(){
        console.log('请求后台')
    }
}
const Credit = {
    __proto__: Request,  // Credit也可以实现继承Request
    total(){
        //super = this.__proto__   this是当前对象,不是调用对象,也就是不是hannie
        console.log(super.ajax() + '积分统计')
    }
}

...其他同上

复制代码

以下根据原型知识,写一个应用实例:Tab切换改变颜色

TAB选项卡显示效果基类开发

function Animation(){}
Animation.prototype.hide = function(){
    this.style.display = 'none'
}
Animation.prototype.show = function(){
    this.style.display = 'block'
}
Animation.prototype.background = function(color){
    this.style.backgroundColor = color
}
let tab = document.querySelector('.tab2')
Animation.prototype.background.call(tab, 'red')
复制代码

好用的TAB业务管理类

function Tab(el){
    this.tab = document.querySelector(el)
    this.links = this.tab.querySelectorAll('a')
}
//继承Animation
extend(Tab, Animation)
Tab.prototype.run = function(){
    this.bindEvent()
}
Tab.prototype.bindEvent = function(){
    this.links.forEach((el,i)=>{
        el.addEventListener('click',()=>{
            this.action(i)
        })
    })
}
Tab.prototype.action = function(){
    this.background.call(this.links[i], 'red')
}
new Tab('.tab2').run()

生成的每个对象是独立的
复制代码

当然还可以继续扩展功能,得到更多灵活的API

总结

  • prototypeObject.isPrototypeOf(object)原型检测
  • this和原型没有关系的
  • 最好不要在原生对象中扩展方法
  • 获取构造函数的方法
  • 继承是原型的继承,而不是改变构造函数的原型
  • 继承实现方式Object.create 和 Parent.prototype,这2种方式的区别
  • 父类方法不够用,可以在子类重写同样的方法,父子存在相同方法,会调用子类的方法;父类的方法协助子类完成方法调用,可以使用Parent.prototype.site()
  • 面向对象的多态实现(使用原型的继承实现):不同的形态响应出不同的结果
  • 使用父类构造函数初始属性(Parent.apply(this.args))
  • 怎么使用原型工厂封装继承
  • 怎么用对象工厂派生对象并实现继承
  • JS语言没用多继承,但可以用mixin实现多继承(定义一些功能的定向,当使用的时候,直接合并到原型中)
  • mixin的内部继承,super = this.__proto__, 比如__proto__: Request

详细可以往上查找,结合实例理解。

原型强大的功能。结合this,可以得到更完美的解答问题。 可以看我的另一篇变量对象、执行上下文、作用域和this大集合

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享