面向对象是现实的抽象方式
- 对象可以将多个关联的数据封装到一起,更好的描述一个事物
- 用对象来描述事物,有利于我们将现实的事物,抽离成代码中某个数据结构
JavaScript的面向对象
JS支持多种编程范式
- 包括函数式编程和面向对象编程
- JS中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成
- key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型
- 如果值是一个函数,那么我们可以称之为是对象的方法
如何创建对象
- new Object()
- 字面量的形式
对属性的操作
属性描述符
- 可以精准的添加或修改对象的属性
- 需要使用Object.defineProperty来进行操作
Object.defineProperty()
-
会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
-
参数
- obj 要定义属性的对象
- prop 要定义或修改的属性的名称或Symbol
- descriptor 要定义或修改的属性描述符 (是一个对象)
-
返回值
- 被传递给函数的对象
属性描述符分类
-
数据属性(Data Properties)描述符(Descriptor)
-
存取属性 (Accessor访问器 Properties)描述符(Descriptor)
configurable enumerable value writable get set 数据描述符 可以 可以 可以 可以 不可以 不可以 存取描述符 可以 可以 不可以 不可以 可以 可以
数据属性描述符
-
Configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,是否可以将它修改为存取属性描述符
- 直接在对象上定义某个属性时,这个属性的Configurable为true
- 通过属性描述符定义某个函数时,这个属性的Configurable为false
-
Enumerable:表示属性是否可以通过for-in或者**Object.keys()**返回该属性
- 直接在对象上定义某个属性时,这个属性的Enumerable为true
- 通过属性描述符定义某个函数时,这个属性的Enumerable为false
-
Writable:表示是否可以修改属性的值
- 直接在对象上定义某个属性时,这个属性的Writable为true
- 通过属性描述符定义某个函数时,这个属性的Writable为false
-
Value:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
- 默认情况下这个值是undefined
存取属性描述符
- Configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,是否可以将它修改为数据属性描述符
- 直接在对象上定义某个属性时,这个属性的Configurable为true
- 通过属性描述符定义某个函数时,这个属性的Configurable为false
- Enumerable:表示属性是否可以通过for-in或者**Object.keys()**返回该属性
- 直接在对象上定义某个属性时,这个属性的Enumerable为true
- 通过属性描述符定义某个函数时,这个属性的Enumerable为false
- get:获取属性时会执行的函数。默认值undefined
- set:设置属性时会执行的函数。默认值undefined
- 应用场景
- 隐藏某一个私有属性不希望直接外界使用和赋值
- 截获某一个属性它访问和设置值的过程时
对象方法补充
getOwnPropertyDescriptor(obj, prop)
- 获取对象的特定属性描述符
getOwnPropertyDescriptors(obj)
- 获取对象的所有属性描述符
Object.preventExtensions(obj)
-
禁止对象扩展新属性
-
给一个对象添加新的属性会失败(严格模式下会报错)
Object.seal(obj)
-
密封对象,禁止对象配置/删除属性
-
实际是调用preventExtensions
-
并将现有属性的configurable: false
-
// for循环的方法来密封对象 for (var key in obj) { Object.defineProperty(obj, key, { configurable: false, enumerator: false, writable: true, value: obj[key] }) }
Object.freeze(obj)
- 冻结对象,禁止属性修改
- 实际上是调用seal
- 并将现有属性的writable:false
创建多个对象的方案
对象字面量 / new Object
- 缺点:重复代码太多
工厂模式
-
function createPerson(name, age, height, address) { var p = {} p.name = name p.age = age p.height = height p.address = address p.eating = function () { console.log(this.name + '在吃东西'); } return p } var p1 = createPerson("zzy", 22, 1.88, "广州") var p2 = createPerson("lll", 24, 1.78, "北京") -
缺点:对象的类型都是Object,获取不到对象最真实的类型
构造函数
- 也成为构造器(constructor),通常会在创建对象时调用
- 在其它面向对象的语言中,构造函数是存在于类中的一个方法,称之为构造方法
JS中的构造函数
- 一个普通函数被使用new操作符来调用,那么这个函数就被称为构造函数
- 函数名首字母大写,多个单词采用驼峰
new操作符调用的作用
- 在内存中创建一个新对象(空对象)
- 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
- 构造函数内部的this,会指向创建出来的新对象
- 构造函数的内部代码(函数体代码)
- 如果构造函数没有返回对象,则返回创建出来的新对象
缺点
- 如果构造函数中有函数,那么执行构造函数时都会创建新的函数对象,浪费空间
对象的原型
- JS中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的属性可以指向另一个对象
- prototype(对象原型)也被称之为隐式原型
- 早期ECMA没有规范如何去查看prototype(对象原型)
查看对象原型
- 浏览器/node提供
__proto__属性去查看prototype(对象原型) - ES5以后官方提供
Object.getPrototypeOf()去查看prototype(对象原型)
对象原型的作用
-
当我们从一个对象中获取某个属性时,会触发get操作
-
在当前对象中去查找对应的属性,如果找到就直接使用
-
如果没有找到,那么就会沿着它的原型链去查找[[prototype]]
-
函数的原型
函数的原型
-
函数作为对象也有[[prototype]](隐式原型)
-
函数还会多出来一个显式原型属性:prototype
-
对象的隐式原型指向函数的显式原型
-
// 函数也是一个对象 function foo() {} console.log(foo.__proto__); // 函数作为对象也有[[prototype]](隐式原型) console.log(foo.prototype); //函数还会多出来一个显示原型属性:prototype var f1 = new foo() f1.__proto__ === foo.prototype // 对象的隐式原型指向函数的显示原型
函数的原型内存图
函数原型上的属性
constructor
-
foo.prototype这个对象中有一个constructor的属性 -
prototype.constructor= 构造函数本身 -
foo.prototype.constructor.name === foo.name
添加自己的属性
-
function foo() {} foo.prototype.name = 'zzy' var f1 = new foo() console.log(f1.name, f1.age); // zzy
修改整个prototype对象
-
真实开发中通过
Object.defineProperty()方法添加constructor -
foo.prototype = { // constructor: foo, //真实开发中通过Object.defineProperty()方法添加constructor name: 'zzy', age: 22, } Object.defineProperty(foo.prototype, "constructor", { enumerable: false, writable: true, configurable: true, value: foo }) var f0 = new foo() console.log(f0.name); // zzy -
赋值为新的对象内存图
-
创建多个对象的方案
原型加构造函数
-
一般属性放在函数自身内
-
函数需要放到原型上
-
function Person(name, age, height) { this.name = name this.age = age this.height = height } Person.prototype.eating = function () { console.log(this.name + '在吃东西'); } var p1 = new Person('zzy', 22, 1.88) var p2 = new Person('lll', 18, 1.70) p1.eating() // zzy在吃东西 p2.eating() // lll在吃东西
JavaScript原型链
- 从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取
Object的原型
Object.prototype
- 是最顶层的原型
- 从Object直接创建出来的对象的原型都是
[Object:null prototype] {}- 该对象的原型属性指向null,也就是最顶层的原型
- 该对象上有很多默认属性和方法
Object是所有类的父类
- 原型链最顶层的原型对象就是Object的原型对象
面向对象的特性
封装
- 将属性和方法封装到一个类中,称之为封装的过程
继承
- 继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态的前提(纯面向对象中)
- 继承可以帮我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可
多态
- 不同的对象在执行时表现出不同的形态
抽象(有争议)
- 将现实的事物抽象为代码的过程
继承
原型链继承
-
弊端
- 原型上面的属性无法枚举,继承的属性在stu对象中看不到
- 获取引用,修改引用中的值会相互影响(向生成的对象中添加新的属性,会影响之后生成的对象,会修改原型)(push)
- 没有传递参数
-
// 父类: 公共属性和方法======================== function Person(name = 'zzy') { this.name = name this.friends = [] } Person.prototype.eating = function () { console.log(this.name + '在吃饭'); } // 子类:特有属性和方法========================== function Stu(sno = '03171242') { this.sno = sno } // 通过原型链的方式,将p中的属性和函数赋值给Stu的prototype var p = new Person() Stu.prototype = p Stu.prototype.learning = function () { console.log(this.name + '在学习'); } var stu = new Stu() console.log(stu.name); stu.eating(); // 原型链方式的弊端: // 1. 第一个弊端:继承的属性在stu对象中看不到,原型上面的属性无法枚举 // console.log(stu.name); // undefined // 2.创建两个stu对象 var stu1 = new Stu() var stu2 = new Stu() stu1.friends.push('lll') // 第二个弊端:会导致之后生成的对象的friends都是'lll' 获取引用,修改引用中的值会相互影响 stu1.name = 'aaa' // 不会影响新生成的对象的name 不会改原型,在本对象内添加属性 console.log(stu1.name); // ['aaa'] console.log(stu2.name); // ['zzy'] // 3.第三个弊端 在前面实现类的过程中都没有传递参数
借用构造函数继承
-
constructor stealing(借用构造函数继承/经典继承/伪造对象) -
在子类型构造函数的内部调用父类型构造函数
-
函数可以在任意时刻被调用
-
可以通过apply()和call()方法在新创建的对象上执行构造函数
-
function Stu(name, age, friends, sno) { Person.call(this, name, age, friends) this.sno = sno }
-
-
// 借用构造函数继承方案============================================================ // 父类: 公共属性和方法======================== function Person(name, age, friends) { //这里的this = stu this.name = name this.age = age this.friends = friends } Person.prototype.eating = function () { console.log(this.name + '在吃饭'); } // 子类:特有属性和方法========================== function Stu(name, age, friends, sno) { Person.call(this, name, age, friends) this.sno = sno } // 通过原型链的方式,将p中的属性和函数赋值给Stu的prototype // var p = new Person() // Stu.prototype = p // Stu.prototype.learning = function () { // console.log(this.name + '在学习'); // } var stu = new Stu("zzy", 22, ['kobe'], 12) // console.log(stu.name); // stu.eating(); // 原型链方式的弊端的解决: // 1. 解决第一个弊端:继承的属性在stu对象中看不到,原型上面的属性无法枚举 console.log(stu); // Person { name: 'zzy', age: 22, friends: [ 'kobe' ], sno: '211' } // 2.创建两个stu对象 var stu1 = new Stu('aaa', 12, ['xiaoa'], 123) var stu2 = new Stu('bbb', 21, ['ddd'], 321) stu1.friends.push('lucy') // 解决第二个弊端:会导致之后生成的对象的friends都是'lll' 获取引用,修改引用中的值会相互影响 console.log(stu1); // Person { name: 'aaa', age: 12, friends: [ 'xiaoa', 'lucy' ], sno: 123 } console.log(stu2); // Person { name: 'bbb', age: 21, friends: [ 'ddd' ], sno: 321 } stu1.name = 'woai' // 不会影响新生成的对象的name 不会改原型,在本对象内添加属性 console.log(stu1.name); // ['woai'] console.log(stu2.name); // ['bbb'] // 3.解决第三个弊端 在前面实现类的过程中都没有传递参数 var stu3 = new Stu('ccc', 24, ['eee'], 021) // 借用构造函数的弊端================== // 1. Person函数至少被调用两次 // 2. stu的原型对象上会多出一部分属性 -
组合继承是JS最常用的继承模式之一
-
组合继承最大的问题是无论什么情况下,都会调用两次父类构造函数
- 在创建子类原型的时候
- 在子类构造函数内部(每次创建子类实例的时候)
-
所有子类实例会拥有两份父类属性,默认访问实例本身这部分
- 在当前的实例里面(
Person本身) - 子类对应的原型对象中(
Person.__proto__)
- 在当前的实例里面(
父类原型赋值给子类继承
- 这种做法是不符合面向对象规范的,
- 修改了子类原型对象的某个引用类型的时候,父类原型对象的引用类型也会被修改
原型式继承函数(针对对象)
-
JSON创立者(道格拉斯·可罗克福德Douglas Crockford),06年写了一篇文章:《Prototypal Inheritance in JavaScript》(在JS中使用原型式继承)
-
obj = { name: "zzy", age: 22, }; // 原型式继承函数,setPrototypeOf() function createObject1(o) { var newObj = {} newObj.__proto__ = o // 真实开发环境不建议使用__proto__ Object.setPrototypeOf(newObj, o) return newObj } var info = createObject2(obj); console.log(info.__proto__); // { name: 'zzy', age: 22 } // 道格拉斯·可罗克福德的原型式继承函数,还没有setPrototypeOf() function createObject2(o) { function Fn() {} Fn.prototype = o var newObj = new Fn(); // newObj.__proto__ = Fn.prototype return newObj; } var info = createObject2(obj); console.log(info.__proto__); // { name: 'zzy', age: 22 } // 最新ECMA提供了方法可以直接实现原型式继承函数的功能,Object.create() var info = Object.create(obj) console.log(info.__proto__); // { name: 'zzy', age: 22 }
寄生式继承函数(针对对象)
-
寄生式(Parasitic)继承是与原型式(Prototypal )继承紧密相关的一种思想,由道格拉斯·可罗克福德(Douglas Crockford)提出和推广
-
寄生式继承的思想是结合原型式继承和工厂模式的一种方式
- 创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回
-
var personObj = { running: function () { console.log("running~"); }, }; //弊端:1.stu的函数会每次都重复创建;2.无法明确对象类型 function createsStudent(name) { var stu = Object.create(personObj); stu.name = name; stu.studying = function () { console.log("studying~"); }; return stu; } var stuObj1 = createsStudent("aaa"); var stuObj2 = createsStudent("bbb"); console.log(stuObj1.name); // aaa stuObj1.studying(); // studying~ stuObj2.running(); // running~
寄生组合式继承(最终方案)
-
封装子类和父类类型继承的两个方法(核心函数)
-
// 社区中用的较多,没有Object.create()方法时 function createObject(o) { function Fn() {} Fn.prototype = o; return new Fn(); } function inheritPrototype(subType, supType) { subType.prototype =createObject(supType.prototype); Object.defineProperty(subType.prototype, "constructor", { enumerable: false, writable: true, configurable: true, value: subType, }); } -
// 使用Object.create() function inheritPrototype(subType, supType) { subType.prototype = Object.create(supType.prototype); Object.defineProperty(subType.prototype, "constructor", { enumerable: false, writable: true, configurable: true, value: subType, }); }
-
-
实现继承的例子
-
// 实现继承 function Person(name, age, friends) { this.name = name; this.age = age; this.friends = friends; } Person.prototype.running = function () { console.log(this.name + "running~"); }; function Student(name, age, friends, sno, score) { Person.call(this, name, age, friends); this.sno = sno; this.score = score; } inheritPrototype(Student, Person); Student.prototype.studying = function () { console.log(this.name + "studying~"); }; var stu = new Student("zzy", 22, ["aa", "bb", "cc"], 111, 100); console.log(stu); stu.running(); stu.studying();
-
原型的判断方法
hasOwnProperty
- 对象是否有某一个属于自己的属性(不是原型上的属性)
- 在当前对象中返回true,只在原型中的属性返回false
in/for in 操作符
- 判断某个属性是否在某个对象或者对象的原型上
- 不管在当前对象还是原型返回的都是true
var obj = {
name: 'zzy',
age: 22
}
var info = Object.create(obj, {
address: {
value: '广州市',
enumerable: true
}
})
console.log(info); // { address: '广州市' }
console.log(info.__proto__); // { name: 'zzy', age: 22 }
// hasOwnProperty方法判断 在当前对象中返回true,只在原型的中的属性返回false
console.log(info.hasOwnProperty('address')); // true
console.log(info.hasOwnProperty('name')); // false
// in 操作符 不管在当前对象还是原型中返回的都是true
console.log('address' in info); // true
console.log('name' in info); // true
for(var key in info) {
console.log(key); // address, name, age
}
instanceof
- 用于检测构造函数的prototype,是否出现在某个实例对象的原型链上
- 会自动调用构造函数的prototype
isPrototypeOf
- 用于检测某个对象,是否出现在某个实例对象的原型链上
function Person(params) {}
var p = new Person();
// 判断Person的prototype是否出现在p的原型链上
console.log(p instanceof Person); // true
// 判断p有没有出现在Person.prototype上
console.log(Person.prototype.isPrototypeOf(p)); // true
var obj = {
name: 'zzy'
}
var info = Object.create(obj)
// console.log(info instanceof obj); // 报错 obj不是构造函数,没有prototype属性
console.log(obj.isPrototypeOf(info)); // true