JS 深入理解Object,必会知识点

目录
文章目录隐藏
  1. 1. new 操作符
  2. 2. 原型链
  3. 3. 继承
  4. 4. instanceof 运算符
  5. 5. Object 的一些函数
  6. 6. 总结

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 空对象添加属性,且默认返回了 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. 原型链

关于原型链知识大家可以看这篇文章《我对 js 中原型原型链的理解(有图有真相)

js 原型链
从图中我们看出几条链路:

链路 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() // 我是张三

优点:

  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 实例也跟着变化
    
  2. 创建子类实例时,无法向父类的构造函数传递参数。
  3. 无法实现多继承,子类的 prototype 属性只能设置一个值。
  4. 为子类添加原型对象上的属性和方法,必须放置继承父类实例之后。

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 // 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 对象的知识点,值得收藏,工作面试必备!

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » JS 深入理解Object,必会知识点

发表回复