本文总结了 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 对象的知识点,值得收藏,工作面试必备!
评论