这是我参与更文挑战的第5天,活动详情查看:更文挑战
[重学JavaScript系列文章连载中...]
本文总结了Object的相关知识点,日常开发或者面试这块都是重中之重,大纲如下:
- new操作符
- 原型链
- 继承
- instanceof运算符
- Object的一些函数
1. new操作符
引用类型的实例都需要通过new操作符来生成,我们先看看创建实例对象都发生了:
function Man(name,age){
this.name = name
this.age = age
}
// 创建Man实例body
let boby = new Man()
/*
* 上述创建body实例的流程如下:
*/
// 1. 创建一个空对象
let body = {}
// 2. 将body空对象的原型链指向Man的原型
body.__proto__ = Man.prototype
// 3. 将Man()函数中的this指向body变量
Man.call(body)
通过上面例子,我们知道:new操作符在执行中改变了this的指向。
更进一步了解函数内的this,若没有return值,则默认return this,例子看下:
function Man(name,age){
console.log(this) // Man{} 空对象
this.name = name
this.age = age
}
// 可以看到实际上是给Man空对象添加属性,且默认返回了this
new Man('张三',18) // {name:'张三',age:18}
// 写个参照,作为比对
function Man(name,age){
let obj = {}
obj.name = name
obj.age = age
}
// 输出Man{}空对象,而属性值是赋值到变量obj。当然若为了得到name|age属性,直接return obj就可以了。
new Man('李四',18) // Man {}
2. 原型链
从图中我们看出几条链路:
链路1:自定义构造函数
f1实例通过__proto__属性指向Foo构造函数的原型对象。
f1.__proto__ = Foo.prototype;
Foo构造函数的原型对象通过__proto__执行Object类型的原型对象。
Foo.prototype.__proto__ = Object.prototype;
Object类型的原型对象通过__proto__指向null
Object.prototype.__proto__ = null;
链路2:系统构造函数|对象字面量创建的对象
new Object().__proto__ = Object.prototype
链路3:函数
function.__proto__ = Function.prototype
Function.prototype.__proto__ = Object.prototype
总结:
对象的原型链最终都指向Object.prototype,对象的构造器最终都指向函数构造器Function。
function Man(){}
Man.prototype.say = function(){}
let boy = new Man()
boy.__proto__ === Man.prototype // true
boy.__proto__.constructor === Man // true
boy.constructor // Man
boy.constructor.prototype === boy.__proto__ // true
3. 继承
在不影响父类对象的情况下,使得子类对象具有父类对象的特性。这里整理几种实现继承的方法,以下将以Man作为父类,总结几种实现继承的方式。
父类
// 作为父类
function Man(name){
// 属性
this.type = 'man'
this.name = name
// 实例方法
this.eat = function(){this.name+'在吃饭'}
}
// 原型函数
Man.prototype.getName = function(){'我是'+this.name}
1. 原型链继承
重写子类的prototype属性,将其指向父类的实例
// 子类 Boy
function Boy(name){
this.name = name
}
// 原型继承,但同时也继承了Man的构造函数,因此需要将Boy的构造函数指向本身
Boy.prototype = new Man()
// Boy的构造函数指向本身
Boy.prototype.constructor = Boy
let boy = new Boy('张三')
boy.type // 'man' 继承父类
boy.name // '张三'
// Boy原型对象指向Man实例,在创建boy实例会继承Man实例的函数和原型方法,在调用boy实例方法时this指向boy实例
boy.eat() // 张三在吃饭
boy.getName() // 我是张三
优点:
- 简单,易于实现,只需设置子类的prototype指向父类的实例。
- 继承关系纯粹,生成的子类既是子类的实例,也是父类的实例。
- 可通过子类直接访问父类原型链属性和函数。
缺点:
- 子类的所有实例将共享父类的属性。这会产生严重问题,若父类属性为引用类型,则某个实例修改了引用类型的数据,其他实例该属性值也将变化。
function Man(){
this.hobbys = ['洗衣','做饭']
}
function Boy(){}
Boy.prototype = new Man()
Boy.prototype.constructor = Boy
let b1 = new Boy()
let b1 = new Boy()
b1.hobbys.push('编码')
b1.hobbys // ['洗衣','做饭','编码']
b2.hobbys // ['洗衣','做饭','编码'] b2实例也跟着变化
- 创建子类实例时,无法向父类的构造函数传递参数。
- 无法实现多继承,子类的prototype属性只能设置一个值。
- 为子类添加原型对象上的属性和方法,必须放置继承父类实例之后。
2. 构造继承
在子类的构造函数中通过call()函数改变this的指向,调用父类的构造函数,从而能将父类的实例的属性和函数绑定到子类的this上。
function Boy(name,age){
// 继承Man实例的属性和方法,并不能继承父类原型函数,子类没有通过某种方式来调用父类原型对象的函数
Man.call(this,name) // 向父类构造函数传参数
this.age = age
}
优点:
- 可以解决子类共享父类属性的问题,每个子类都生成了自己继承自父类的属性和方法。
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承,在子类的构造函数多次调用call()函数来继承多个父类对象。
缺点:
- 实例只是子类的实例,并不是父类的实例。因为并为通过原型对象将子父类串联,所以生成的实例跟父类没有关系,这也失去了继承的意义。
- 只能继承父类实例的属性和方法,并不能继承原型对上的属性和方法。
- 无法复用父类的实例函数,导致子类实例都拥有父类实例函数的引用,造成内存消耗,影响性能。
3. 复制继承
首先生成父类的实例,然后通过遍历父类实例的属性和函数,并依次设置为子类实例的属性和函数或者原型上的属性和函数。
function Boy(name,age){
let man = new Man(name)
// 父类的属性和方法,全部添加到子类
for(let p in man){
if(man.hasOwnProperty(p)){ // 实例的属性和方法,返回true
this.[p] = man[p]
}else{
Boy.prototype[p] = man[p]
}
}
// 子类自己的属性
this.age = age
}
优点:
- 支持多继承
- 能同时继承父类实例的属性和函数以及原型对象上的属性和函数
- 可以向父类构造函数传参
缺点:
- 父类所有的属性都要复制,消耗内存
- 实例只是子类的实例,并不是父类的实例,并没有通过原型链串联起父子类
4. 组合继承
【推荐】组合了构造继承和原型链继承两种方法。一方面在子类构造函数通过call()函数调用父类构造函数,将父类实例的属性和方法绑定到子类的this上;另一方面,通过改变子类的prototype属性,继承父类原型对象上的属性和方法。
function Boy(name,age){
// 通过构造函数继承父类实例的属性和方法
Man.call(this,name)
this.age = age
}
// 通过原型继承父类原型上的属性和方法
Boy.prototype = new Man()
Boy.prototype.constructor = Boy
优点:
- 既能继承父类实例的属性和方法,也能继承原型对象上的属性和方法
- 既是子类的实例,也是父类的实例
- 不存在引用共享的问题
- 可以向父类的构造函数参数
缺点: 父类的实例属性会被绑定两次,一次是在子类构造函数中,通过call()函数调用父类构造函数,另一次是在子类prototyoe属性改写时,调用了一次父类构造函数。
5. 寄生组合
【最优】在子类进行子类的prototype设置时,去掉父类实例的属性和方法
function Boy(name,age){
Man.call(this,name)
this.age = age
}
(function(){
let S = function(){}
// S函数的原型指向父类Man的原型,去掉父类的实例属性,从而避免父类实例属性的2次绑定
S.prototype = Man.prototype
Boy.prototype = new S()
Boy.prototype.constructor = Boy
})()
4. instanceof运算符
target instanceof constructor
表示:target对象是不是构造函数constructor的实例。
先来看段instanceof运算符实现原理比较经典的JS代码解释。
/*
* instanceof 运算符实现原理
* L: 表示左表达式 R: 表示右表达式
*/
function instanceof(L,R){
let O = R.prototype
L = L.__proto__
while(true){
if(L===null)
return false
if(L === O)
return true
L = L.__proto__ // 递归L的__proto__属性
}
}
以下看些例子:
// 基础用法
function Man(){}
let m = new Man()
m instanceof Man // true m.__proto__ === Man.prototype
// 继承判断
function Boy(){}
Boy.prototype = new Man()
let b = new Boy()
b instanceof Man // true ,通过集成,Man.prototype 出现在Boy的原型链上
// 复杂用法
Object instanceof Object // true
Function instanceof Function // true
String instanceof String // false
// 解释下 String instanceof String 返回false的判断过程
取值: L = String.__proto__ = Function.prototype ; R = String.prototype
第一次判断:L !== R,返回false
继续取L.__proto__: L = Function.prototype.__proto__ = Object.prototype
第二次判断:L !== R, 返回false
继续取L.__proto__: L = Object.prototype.__proto__ = null
再次判断:L === null ,返回false
5. Object的一些函数
1. hasOwnProperty()
判断对象自身是否拥有指定名称的实例属性,不会检查实例对象原型上的属性。
function Man(name){
this.name = name
}
Man.prototype.say = function(){}
const boy = new Man('张三')
boy.hasOwnProperty('name') // 实例上的属性:true
boy.hasOwnProperty('toString') // 原型上的属性:false
2. Object.create()
创建并返回一个指定原型和指令属性的对象。语法如下:
Object.create(prototype,propertyDescriptor)
prototype属性为对象的原型,可以为null,若未null,则对象的原型为undefined。
propertyDescriptor属性描述符格式如下:
propertyName:{
// 属性值
value:'',
// 是否可写,若为false,则值读
writable:true,
// 是否可枚举,默认false
enumerable:false,
// 是否可配置,如:修改属性的特性,是否可以删除属性,默认false
configurable:true
}
举个例子深入理解下:
let boy = {name:'张三'}
let obj = Object.create(boy) // 输出: obj {}
console.log(obj.name) // 输出:张三,可以看出boy被挂载到原型上了
// 通过polyfill下Object.create()的实现
Object.create = function(proto,propertiesObj){
function F(){}
F.prototype = proto
// 其他代码省略
return new F()
}
let boy = {name:'张三'}
let obj = Object.create(boy)
obj.__proto__name === boy.name // true
3. Object.defineProperties()
添加或者修改对象的属性值。
let boy = {}
Object.defineProperties(boy,{
name:{ // 跟Object.create()的属性描述符一样
value:18,
writable:true
}
})
4. Object.getOwnPropertyNames()
获取对象的所有实例属性和函数,不包含原型链继承的属性和函数。
function Man(name){
this.name = name
this.getName = function(){return this.name}
}
Man.prototype.say = function(){}
let boy = new Man('张三')
Object.getOwnPropertyNames(boy) // ['name','getName']
5. Object.keys()
获取对象可枚举的实例属性,不包含原型链继承的属性。
let obj = {
name:'张三',
getName:function(){}
}
Object.keys(obj) // ['name','getName']
// 设置name属性不可枚举
Object.defineProperty(obj,'name',{enumerable:false})
Object.keys(obj) // ['getName']
6. 总结
至此我们总结了Object对象的知识点,值得收藏,工作面试必备!