写点什么

重学 JS | 深入理解 Object,必会知识点汇总

用户头像
梁龙先森
关注
发布于: 2021 年 01 月 06 日
重学JS | 深入理解Object,必会知识点汇总

本文总结了 Object 的相关知识点,日常开发或者面试这块都是重中之重,大纲如下:

  1. new 操作符

  2. 原型链

  3. 继承

  4. instanceof 运算符

  5. 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空对象添加属性,且默认返回了thisnew 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 // trueboy.__proto__.constructor === Man // trueboy.constructor // Manboy.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 属性,将其指向父类的实例

// 子类 Boyfunction 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() // 我是张三
复制代码

优点

  1. 简单,易于实现,只需设置子类的 prototype 指向父类的实例。

  2. 继承关系纯粹,生成的子类既是子类的实例,也是父类的实例。

  3. 可通过子类直接访问父类原型链属性和函数。

缺点

  1. 子类的所有实例将共享父类的属性。这会产生严重问题,若父类属性为引用类型,则某个实例修改了引用类型的数据,其他实例该属性值也将变化。

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实例也跟着变化
复制代码
  1. 创建子类实例时,无法向父类的构造函数传递参数。

  2. 无法实现多继承,子类的 prototype 属性只能设置一个值。

  3. 为子类添加原型对象上的属性和方法,必须放置继承父类实例之后。

2. 构造继承

在子类的构造函数中通过 call()函数改变 this 的指向,调用父类的构造函数,从而能将父类的实例的属性和函数绑定到子类的 this 上。

function Boy(name,age){  // 继承Man实例的属性和方法,并不能继承父类原型函数,子类没有通过某种方式来调用父类原型对象的函数  Man.call(this,name) // 向父类构造函数传参数	this.age = age}
复制代码

优点

  1. 可以解决子类共享父类属性的问题,每个子类都生成了自己继承自父类的属性和方法。

  2. 创建子类实例时,可以向父类传递参数

  3. 可以实现多继承,在子类的构造函数多次调用call()函数来继承多个父类对象。

缺点

  1. 实例只是子类的实例,并不是父类的实例。因为并为通过原型对象将子父类串联,所以生成的实例跟父类没有关系,这也失去了继承的意义。

  2. 只能继承父类实例的属性和方法,并不能继承原型对上的属性和方法。

  3. 无法复用父类的实例函数,导致子类实例都拥有父类实例函数的引用,造成内存消耗,影响性能。

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}
复制代码

优点

  1. 支持多继承

  2. 能同时继承父类实例的属性和函数以及原型对象上的属性和函数

  3. 可以向父类构造函数传参

缺点

  1. 父类所有的属性都要复制,消耗内存

  2. 实例只是子类的实例,并不是父类的实例,并没有通过原型链串联起父子类

4. 组合继承

【推荐】组合了构造继承和原型链继承两种方法。一方面在子类构造函数通过 call()函数调用父类构造函数,将父类实例的属性和方法绑定到子类的 this 上;另一方面,通过改变子类的 prototype 属性,继承父类原型对象上的属性和方法。

function Boy(name,age){  // 通过构造函数继承父类实例的属性和方法	Man.call(this,name)  this.age = age}// 通过原型继承父类原型上的属性和方法Boy.prototype = new Man()Boy.prototype.constructor = Boy
复制代码

优点

  1. 既能继承父类实例的属性和方法,也能继承原型对象上的属性和方法

  2. 既是子类的实例,也是父类的实例

  3. 不存在引用共享的问题

  4. 可以向父类的构造函数参数

缺点

父类的实例属性会被绑定两次,一次是在子类构造函数中,通过 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 // trueFunction instanceof Function // trueString 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 对象的知识点,值得收藏,工作面试必备!


发布于: 2021 年 01 月 06 日阅读数: 47
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
重学JS | 深入理解Object,必会知识点汇总