JS 深入理解Object,必会知识点
本文总结了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. 原型链
关于原型链知识大家可以看这篇文章《我对 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() // 我是张三
优点:
- 简单,易于实现,只需设置子类的 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 对象的知识点,值得收藏,工作面试必备!
码云笔记 » JS 深入理解Object,必会知识点