原型
在JS
中,实现继承是通过原型来完成的,同时原型还可以实现其他相关的应用,前期我们需要学习原型,后期我们一般使用class
语法糖,其实质还是原型
原型可以通过现实中的例子进行举例:在现实生活中,比如我们想要开车,但是刚刚毕业我们没有钱去买车,如果我们的父母有车,我们就可以开我们父母的车,父母就是我们计算机中的原型
我们在控制台打印的内容,如一个对象,我们将其点开,可以看到__proto__
,这个属性就是该对象的父级属性,我们称之为原型,继续点开后可以看到父级所拥有的相关属性,父亲属性可能还有其下层的父级原型,我们可以通过原型链来进行统一的描述:自己-父亲-爷爷,有些内容是只有两代的
获取对象的原型:
let hd = {};
console.log(Object.getPrototypeOf(hd));
没有原型的对象也是存在的,但是我们使用较少,我们可以通过create
进行创建对象,并指定其父级为null
,这种形式创建的对象就是完全数据字面量的对象
let hd = Object.create(null, {
name: {
value: 'jlc'
}
})
原型方法与对象方法的优先级
对象有一个属性__proto__
来记录他的原型,也就是他的父级
let hd = {};
console.dir(hd.__proto__);
如果对象没有某些功能,但是父级有这个功能,我们就可以将这个功能继承过来使用,如果某个功能该对象没有,其长辈也没有,当我们使用这个没有的功能的时候,控制台就会报错:hd.show is not a function
在对象和原型中添加方法:
// 对象中添加方法
let hd = {
show() {
console.log("jlc");
}
};
// 原型中添加方法
hd.__proto__.show = function() {
console.log("jlc");
}
如果后续添加的方法中,在对象中有这个方法,在原型中也有这个方法,当我们调用时,会优先执行对象中的方法
函数可以拥有多个原型长辈
函数是一个特殊的对象,但是函数可以拥有多个长辈原型,__proto__
是一个原型长辈,prototype
这个也是一个长辈,但是每个长辈的使用场景往往是不一样的,当函数作为对象调用的时候,我们直接找__proto__
这个原型即可;
function User() {}
User.__proto__.view = function() {
consloe.log("jlc");
}
User.view();
当函数作为构造函数创建出的对象,这个新的对象会默认指定到的父级原型是prototype
:
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
这个构造函数产生的实例来进行使用的)
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
对象,是没有父级原型的
系统构造函数的原型体现
大部分数据类型都是由构造函数创建的,所以构造函数的prototype
成为他的父级
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
当我们为这构造函数的原型添加方法,就会影响所有新增的对象
自定义对象的原型
我们可以自定义某个对象的原型,设置某个对象的原型为我们指定的一个对象
let hd = { name: "hd" };
let parent = { name: "parent", show(){
console.log(this.name)
} }
Object.setPrototypeOf(hd, parent);
console.log(hd);
hd.show() // this始终是我们调用的对象
设置完自定义原型后,父级有什么方法,子级都可以进行继承使用
我们可以通过Object.getPrototypeOf(hd)
来进行查看对象的原型
原型中的constructor
我们可以通过原型中的constructor
找到其构造函数
constructor
属性对应的值就是该对象的构造函数
function User() {}
console.log(User.prototype.constructor == User)
// 结构返回true
通俗的讲,constructor
就是为了将实例的构造器的原型对象暴露出来, 比如你写了一个插件,别人得到的都是你实例化后的对象, 如果别人想扩展下对象,就可以用instance.constructor.prototype
去修改或扩展原型对象
当然,我们使用原型,其核心是使用这个原型内部的功能方法
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
来进行创建新的对象:
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
原型链
原型链可以理解为一步一步找上去的过程,像一个链条的过程
let arr = [];
console.log(arr.__proto__.__proto__ == Object.prototype) // true
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
的原型链上有没有 b
的prototype
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
instanceof
和 Object.isPrototypeOf
的区别:前者判断的是构造函数的prototype
是否在某个对象的原型链上,后者直接判断某个对象是否在另一个对象的原型链上(检测某个对象是否为另一个对象原型链中的一部分,即一个对象是否为另一个对象的长辈)
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
对象中是否有某个属性,常用的检测属性的方法有:in
和hasOwnProperty
,前者检测属性的范围不仅是检测这个对象是否具有这个属性,还检测其对象的原型链上是否有这个属性;后者只检测当前的对象是否有这个属性,不会涉及到原型链
let a = { name: "jlc" };
Object.prototype.web = "baidu.com";
console.log("web" in a); // true
console.log(a.hasOwnProperty("web")); // fasle
遍历某个对象的所有属性:
let a = { name: "jlc", age: "24" };
for (const key in a) {
for (a.hasOwnProperty(key)) {
console.log(key);
}
}
借用原型链
原型的作用是指可以让子级借用长辈中的方法属性,如果当前对象的长辈原型链中没有需要的方法功能,但是在另一个对象的原型链中有当前对象需要的功能方法,我们可以使用借用原型链去实现:
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
的方法的,我们想要过滤元素只能进行借用:
<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>
合理的使用规则链
在对象定义方法的时候,我们一般将自定义的方法写在原型中,方便复用,不给每个对象单独的配置方法,减少内存的开销:
jsfunction 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
始终指向的是调用该属性的对象,不会因为原型链而产生变化jsconst User = { name:'user', } const Person = { name:'person', age:18, show(){ console.log(this.name) } } Object.setPrototypeOf(User, Person) // this始终指向调用者 User.show() // user
不要滥用原型:不要在系统的原型上追加方法,造成冲突问题,比如我们在系统的原型上写一个方法,后续有使用外置组件库,他也有这个方法,那么就会造成那个后面加载引入就会使用哪个,使代码不稳定
__proto__
之前都是讨论的prototype
,是用来定义构造函数的原型
为单个对象更改它的原型的进化过程:
使用
Object.create
jslet user = { show() { return this.name; } }; // 添加参数作为新对象hd的原型 let hd = Object.create(user, { name: { value: "jlc" } }); // 这样hd的原型就变成了user对象 console.log(hd.show()); // jlc
这种方式只能定义对象的原型,获取对象的原型比较麻烦
后续更新后可以通过
Object.create
方法来获取原型__proto__
为了方便获取对象原型自主开发(不是官方的)的一个为单个对象更改原型的方法,
hd.__proto__
表示有值的时候获取对象的原型,没有值的时候设置对象的原型jslet user = { show() { return this.name; } }; let hd = { name: "jlc" }; // 添加参数作为新对象hd的原型 hd.__proto__ = user; console.log(hd.show()); // jlc // 获取hd的原型对象 console.log(hd.__proto__);
setPrototype
setPrototype
是官方给出用来代替__proto__
的,推荐后续使用官方的方法jslet 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__
并不是一个严格意义上的属性,实质上是属性访问器,是getter
和setter
,会对设置的值进行自动判断,只有设置的值是对象才可以进行设置,否则设置是没有效果的(会被属性访问器拦截下来)
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__); // 设置的是数值,原型不会改变
上述的拦截实现可以概括为:
let hd = {
action: {},
get proto() {
return this.action;
},
set proto(obj) {
if (obj instanceof Object) {
this.action = obj;
}
}
};
我们在打印其对象原型中可以在__proto__
内部看到get __proto__
和set __proto__
的属性过滤方法
如果我们就想要设置这个非对象的属性,我们可以先设置这个对象没有这个原型(原型为空)即可:
let hd = Object.create(null);
hd.__proto__ = "jlc";
console.log(hd.__proto__); // jlc