Skip to content

原型

JS中,实现继承是通过原型来完成的,同时原型还可以实现其他相关的应用,前期我们需要学习原型,后期我们一般使用class语法糖,其实质还是原型

原型可以通过现实中的例子进行举例:在现实生活中,比如我们想要开车,但是刚刚毕业我们没有钱去买车,如果我们的父母有车,我们就可以开我们父母的车,父母就是我们计算机中的原型

我们在控制台打印的内容,如一个对象,我们将其点开,可以看到__proto__,这个属性就是该对象的父级属性,我们称之为原型,继续点开后可以看到父级所拥有的相关属性,父亲属性可能还有其下层的父级原型,我们可以通过原型链来进行统一的描述:自己-父亲-爷爷,有些内容是只有两代的

获取对象的原型:

js
let hd = {};
console.log(Object.getPrototypeOf(hd));

没有原型的对象也是存在的,但是我们使用较少,我们可以通过create进行创建对象,并指定其父级为null,这种形式创建的对象就是完全数据字面量的对象

js
let hd = Object.create(null, {
    name: {
        value: 'jlc'
    }
})

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

对象有一个属性__proto__来记录他的原型,也就是他的父级

js
let hd = {};
console.dir(hd.__proto__);

如果对象没有某些功能,但是父级有这个功能,我们就可以将这个功能继承过来使用,如果某个功能该对象没有,其长辈也没有,当我们使用这个没有的功能的时候,控制台就会报错:hd.show is not a function

在对象和原型中添加方法:

js
// 对象中添加方法
let hd = {
    show() {
        console.log("jlc");
    }
};

// 原型中添加方法
hd.__proto__.show = function() {
    console.log("jlc");
}

如果后续添加的方法中,在对象中有这个方法,在原型中也有这个方法,当我们调用时,会优先执行对象中的方法


函数可以拥有多个原型长辈

函数是一个特殊的对象,但是函数可以拥有多个长辈原型,__proto__是一个原型长辈,prototype这个也是一个长辈,但是每个长辈的使用场景往往是不一样的,当函数作为对象调用的时候,我们直接找__proto__这个原型即可;

js
function User() {}
User.__proto__.view = function() {
    consloe.log("jlc");
}
User.view();

当函数作为构造函数创建出的对象,这个新的对象会默认指定到的父级原型是prototype

js
function User() {}
User.prototype.show = function() {
    consloe.log("jlc");
}
let hd = new User();
hd.show();

prototype一般是服务于函数实例化对象的

当然,在系统中有很多的构造函数,如String, Number, Object等等,对于这些构造函数(打印看出console.dir(Object)),一样有两个原型长辈__proto__(通过这个对象直接调用的)和prototype(通过new这个构造函数产生的实例来进行使用的)

js
let hd = new Object();
hd.name = 'jlc';
Object.prototype.show = function() {
    console.log("111");
}
hd.show();

对于User这个定义的函数:function User() {},点开prototype原型,我们可以看到该原型下有父级原型__proto__,这个父级原型就是Object,可以看到Object.prototype.show定义的show方法,所以说,函数User的原型(prototype的父级就是Object的原型),同时User的原型(__proto__的父级也是Object的原型),使用的是一个父级,即:User.prototype.__protp__ == User.__proto__.__proto__

我们自己创建函数的父级都指向Object对象的原型,因此,通过函数实例化出来的对象,也可以调用我们Object构造出来的方法

对于Object对象,是没有父级原型的

image-20241004091959046


系统构造函数的原型体现

大部分数据类型都是由构造函数创建的,所以构造函数的prototype成为他的父级

js
const obj = {}  // 内部实现是通过new Object来实现的
console.log(obj.__proto__ == Object.prototype)  // true

const arr = []  // new Array
console.log(arr.__proto__ == Array.prototype)  // true

const str = ''  // new String
console.log(str.__proto__ == String.prototype)  // true

const bool = false  // new Boolean
console.log(bool.__proto__ == Boolean.prototype)  // true

const fun = ()=> {}  // new Function
console.log(fun.__proto__ == Function.prototype)  // true

const reg = /\d+/g  // new RegExp
console.log(reg.__proto__ == RegExp.prototype)  // true

在构造函数创建的时候会有一个原生的对象,这个原生的对象会自动的把这个原型设置成这个构造函数的prototype

当我们为这构造函数的原型添加方法,就会影响所有新增的对象


自定义对象的原型

我们可以自定义某个对象的原型,设置某个对象的原型为我们指定的一个对象

js
let hd = { name: "hd" };
let parent = { name: "parent", show(){
    console.log(this.name)
} }
Object.setPrototypeOf(hd, parent);
console.log(hd);
hd.show()  // this始终是我们调用的对象

image-20241004093314748

设置完自定义原型后,父级有什么方法,子级都可以进行继承使用

我们可以通过Object.getPrototypeOf(hd)来进行查看对象的原型


原型中的constructor

我们可以通过原型中的constructor找到其构造函数

constructor属性对应的值就是该对象的构造函数

js
function User() {}
console.log(User.prototype.constructor == User)
// 结构返回true

通俗的讲,constructor就是为了将实例的构造器的原型对象暴露出来, 比如你写了一个插件,别人得到的都是你实例化后的对象, 如果别人想扩展下对象,就可以用instance.constructor.prototype 去修改或扩展原型对象

当然,我们使用原型,其核心是使用这个原型内部的功能方法

js
function User(name){
    this.name = name
}
// 为原型增加功能方法,进行多个的添加
User.prototype = {
    constructor:User,  // 必须要加,否则会报错
    show(){
        console.log(this.name)
    }
}

// 我们可以通过constructor来进行对象的创建
const lisi = new User.prototype.constructor('aaaa')
// 等价于 const lisi = new User('aaaa')
lisi.show()  //aaaa

因此,给出一个对象,我们就可以通过这个对象创建出一个新的对象,上述方法之外,我们还可以通过自定义createByObject来进行创建新的对象:

js
function User(name) {
    this.name = name;
    this.show = function() {
        console.log(this.name);
    };
}
let hd = new User("JLC");

function createByObject(obj, ...args) {
    const constructor = Object.getPrototypeOf(obj).constructor;
    return new constructor(...args);
}
let hd1 = createByObject(hd, "jlc");
hd1.show();  // jlc

原型链

原型链可以理解为一步一步找上去的过程,像一个链条的过程

js
let arr = [];
console.log(arr.__proto__.__proto__ == Object.prototype) // true
js
let a = { name: "a" }
let c = { name: "c" }
let b = {
    name: "b",
    show() {
        console.log(this.name);
    }
};
Object.setPrototypeOf(a, b); // 把b设置为a的原型
Object.setPrototypeOf(c, b); // 把b设置为c的原型
a.show() // a

注意,不能让两个对象互相将对方设置为其的原型,继承关系只能是单向的

这样,我们就可以将一些公用的方法写到父级里面,方便多个子级去进行调用


原型的检测

instanceof

我们可以通过instanceof方法来进行原型链的检测,简而言之:

a instanceof b表示,a 的原型链上有没有 bprototype

js
function A() {};
let a = new A();
// a 的原型是 A,A 的原型是 Object,也就是说 a 的原型链上有 A 和 Object
// 判断 a 的原型链上有没有 A
console.log(a instanceof A);  // true
// 判断 a 的原型链上有没有 Object
console.log(a instanceof Object);  // true

instanceof 的本质就是在原型族谱上找人

原型链的检测是往上进行查找的,如果往下找是找不到的,只会返回Flase:如console.log(A instanceof a);

isPrototypeOf

instanceofObject.isPrototypeOf的区别:前者判断的是构造函数的prototype是否在某个对象的原型链上,后者直接判断某个对象是否在另一个对象的原型链上(检测某个对象是否为另一个对象原型链中的一部分,即一个对象是否为另一个对象的长辈)

js
let a = {};
let b = {};
console.log(b.instanceof(a));  // false
console.log(Object.prototype.instanceof(a));  //true
console.log(b.__proto__.instanceof(a));  //true

Object.setPrototypeOf(a, b);  // 将b设置为a的原型
console.log(b.instanceof(a));  // true

属性的检测

属性的检测是指判断a对象中是否有某个属性,常用的检测属性的方法有:inhasOwnProperty,前者检测属性的范围不仅是检测这个对象是否具有这个属性,还检测其对象的原型链上是否有这个属性;后者只检测当前的对象是否有这个属性,不会涉及到原型链

js
let a = { name: "jlc" };
Object.prototype.web = "baidu.com";
console.log("web" in a);   // true
console.log(a.hasOwnProperty("web"));  // fasle

遍历某个对象的所有属性:

js
let a = { name: "jlc", age: "24" };
for (const key in a) {
    for (a.hasOwnProperty(key)) {
        console.log(key);
    }
}

借用原型链

原型的作用是指可以让子级借用长辈中的方法属性,如果当前对象的长辈原型链中没有需要的方法功能,但是在另一个对象的原型链中有当前对象需要的功能方法,我们可以使用借用原型链去实现:

js
const obj = {
    data: [1,2,3,4]
}
Object.setPrototypeOf(obj, {
    max(datas){
        // 先大到小排序,在取第一个获取最大值
        return datas.sort((a,b)=> b-a)[0]
    }
})

console.log(obj.max(obj.data)) // 4

const pool = {
    list: [2,3,455,67]
}
console.log(obj.max.call(null, pool.list))  // 455
console.log(obj.max(pool.list)) // 455

// 我们也可以通过借用Math.max()中的方法取得最大值
console.log(Math.max.apply(null, obj.data)); // 4
console.log(Math.max.call(null, ...obj.data)); // 4

apply第一个参数是this指向某个对象 ,第二个参数作为一个数组传参;call 第一个参数是this指向 某个对象,第二个参数可以使用展开语法分散传参

DOM节点借用Array数组中的方法来过滤元素,DOM元素是没有filter的方法的,我们想要过滤元素只能进行借用:

html
<body>
    <button>1</button>
    <button class="btn">2</button>
</body>
<script>
    let btns = document.querySelectorAll("button")
    btns=Array.prototype.filter.call(btns, item => {
        return item.hasAttribute('class')
    })
    console.log(btns)
</script>

合理的使用规则链

  • 在对象定义方法的时候,我们一般将自定义的方法写在原型中,方便复用,不给每个对象单独的配置方法,减少内存的开销:

    js
    function User(name) {
        this.name = name;
    }
    User.prototype.show = function() {
        console.log(this.name);
    }
    let lisi = new User("lisi");
    let jlc = new User("jlc");
    jlc.show();  // jlc
  • this和原型是没有关系的,this始终指向的是调用该属性的对象,不会因为原型链而产生变化

    js
    const User = {
        name:'user',
    }
    const Person = {
        name:'person',
        age:18,
        show(){
           console.log(this.name)
        }
    }
    
    Object.setPrototypeOf(User, Person)
    // this始终指向调用者
    User.show()  // user
  • 不要滥用原型:不要在系统的原型上追加方法,造成冲突问题,比如我们在系统的原型上写一个方法,后续有使用外置组件库,他也有这个方法,那么就会造成那个后面加载引入就会使用哪个,使代码不稳定


__proto__

之前都是讨论的prototype,是用来定义构造函数的原型

为单个对象更改它的原型的进化过程:

  1. 使用Object.create

    js
    let user = {
        show() {
            return this.name;
        }
    };
    // 添加参数作为新对象hd的原型
    let hd = Object.create(user, {
        name: {
            value: "jlc"
        }
    });
    // 这样hd的原型就变成了user对象
    console.log(hd.show());  // jlc

    这种方式只能定义对象的原型,获取对象的原型比较麻烦

    后续更新后可以通过Object.create方法来获取原型

  2. __proto__

    为了方便获取对象原型自主开发(不是官方的)的一个为单个对象更改原型的方法,hd.__proto__表示有值的时候获取对象的原型,没有值的时候设置对象的原型

    js
    let user = {
        show() {
            return this.name;
        }
    };
    let hd = { name: "jlc" };
    // 添加参数作为新对象hd的原型
    hd.__proto__ = user;
    console.log(hd.show());  // jlc
    // 获取hd的原型对象
    console.log(hd.__proto__);
  3. setPrototype

    setPrototype是官方给出用来代替__proto__的,推荐后续使用官方的方法

    js
    let user = {
        show() {
            return this.name;
        }
    };
    let hd = { name: "jlc" };
    // 添加参数作为新对象hd的原型
    Object.setPrototypeOf(hd, user);
    console.log(hd.show());  // jlc
    // 获取hd的原型对象
    console.log(Object.getPrototypeOf(hd));

__proto__并不是一个严格意义上的属性,实质上是属性访问器,是gettersetter,会对设置的值进行自动判断,只有设置的值是对象才可以进行设置,否则设置是没有效果的(会被属性访问器拦截下来)

js
let hd = { name: "jlc" };
hd.__proto__ = {
    show() {
        return this.name;
    }
};
console.log(hd.show());  // jlc
console.log(hd.__proto__);  // 原型也可以正常的获取到

hd.__proto__ = 99;
console.log(hd.__proto__);  // 设置的是数值,原型不会改变

上述的拦截实现可以概括为:

js
let hd = {
    action: {},
    get proto() {
        return this.action;
    },
    set proto(obj) {
        if (obj instanceof Object) {
            this.action = obj;
        }
    }
};

我们在打印其对象原型中可以在__proto__内部看到get __proto__set __proto__的属性过滤方法

如果我们就想要设置这个非对象的属性,我们可以先设置这个对象没有这个原型(原型为空)即可:

js
let hd = Object.create(null);
hd.__proto__ = "jlc";
console.log(hd.__proto__);  // jlc

Released under the MIT License.