1. 创建对象的方式
1、对象字面量的方式
let obj = {
name: 'jenny',
age: 18,
run: function() {
return `${this.name} is running`
}
}
字面量的方式本质上与调用 new Object() 构造函数相同
let obj = new Object()
obj.name = 'jenny'
缺点:对于创建大量相似对象的时候,会产生大量的重复代码
2、工厂模式
function createObj(name, aga) {
return {
name: name,
age: age,
run: function() {
return `${this.name} is running`
}
}
}
const obj1 = createObj('jenny', 18)
const obj2 = createObj('kiki', 20)
工厂模式可以看做是生成对象字面量的机器。主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。
缺点:创建出来的对象获取不到对象最真实的类型(打印出来显示都是Object类型),它只是简单的封装了复用代码,而没有建立起对象和类型间的关系
工厂函数生产的实例带有特定的自定义属性("name", "age"),因此不适合作为原型使用,也不适用于继承
使用案例:使用Object.assign组合对象,来扩展对象的属性或方法
const mvp = {award: "MVP"}
const newObj = Object.assign(obj1, mvp)
3、使用对象字面量作为原型创建实例(寄生?)
使用 Object.create() 方法,新创建出来的对象的__proto__会指向传入的对象
let obj = {
name: 'jenny',
age: 18,
run: function() {
return `${this.name} is running`
}
}
let newObj = Object.create(obj)
newObj.height = '1.80'
console.log(newObj);
这种方式基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
4、构造函数的方式创建对象
// 规范:构造函数的首字母一般是大写
function Person(name, age) {
this.name = name
this.age = age
this.run = function() {
console.log(this.name + 'is running');
}
}
let p1 = new Person('jenny', 18)
let p2 = new Person('kiki', 20)
console.log(p1);
// Person {name: 'jenny', age: 18, run: ƒ}
// 可以看到是 Person 类型(实际上是constructor的name属性)
构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。
但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为相同的方法应该是所有的实例都可以通用的。
5、原型结合构造函数的方式
因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。
因此把公用的属性和方法添加在原型上,可以解决不必要的函数对象创建的问题
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.run = function() {
console.log(this.name + 'is running');
}
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.run = function() {
console.log(this.name + 'is running');
}
let p1 = new Person('jenny', 18)
p1.run() // jennyis running
6、class
- 在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类
- 但是类的本质上只是构造函数,原型链的语法糖
- 最终还是会被 babel 工具转换为ES5的代码
class createObj {
constructor(name, age) {
this.name = name;
this.age = age
}
run() {
return this.name + "is running"
}
}
let obj = new createObj("jenny", 18)
2. 对象继承的方式有哪些
1、原型链的方式实现继承
// 父类
function Person() {
this.name = "Jackson",
this.friends = []
}
Person.prototype.eating = function() {
console.log(this.name + "is eating");
}
// 子类:特有属性和方法
function Student() {
this.sno = 111
}
// 使用父类构造函数创建实例,赋给子类的原型
let p = new Person() // p.__proto__ === Person.prototype
Student.prototype = p
// Student.prototype.__proto__ === Person.prototype
// 创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()
// 获取引用,修改引用中的值,会相互影响
// 因为这个fre1被加到p对象,而stu1,stu2的__proto__都指向p
stu1.friends.push("fre1")
console.log(stu1.friends); // [ 'fre1' ]
console.log(stu2.friends); // [ 'fre1' ]
缺点:实例的类型是不能直观看到的;在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱;还有就是在创建子类型的时候不能向父类传递参数
2、使用构造函数结合原型链的方式实现继承
// 父类
function Person(name, age, friends) {
this.name = name,
this.age = age
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + "is eating");
}
// 子类
function Student(name, age, friends, sno) {
// 借用构造函数的调用
// 我们在这里调用Person,并把需要Person处理的参数传过去
// this 就是Student的实例
Person.call(this, name, age, friends) // 这里可以获得父类的属性
this.sno = sno
}
// 原型链方式
let p = new Person() // 依然需要这里来获得父类的方法
Student.prototype = p
// 创建student的实例
let stu = new Student('jenny', 18, [], 11)
console.log(stu);
这种方式解决了方式一的弊端,但依然存在问题:由于我们是以父类的实例来作为子类型的原型,所以调用了两次父类的构造函数(Person.call、new Person()),造成了子类型的原型中多了很多不必要的属性。
3、原型式继承
原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。
这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。
var obj = {
name: "Jackson",
age: 18
}
// 方式1:原型式继承函数
function createObject(o) {
var newObj = {}
// 这个方法是把o设置为newObj的原型
Object.setPrototypeOf(newObj, o)
return newObj
}
// 方式2:Douglas 的实现(当时还没有setPrototypeOf这个方法)
function createObject2(o) {
function Fn() {}
Fn.prototype = o
var newObj = new Fn()
// 因此 newObj.__proto__ = Fn.prototype = o
return newObj
}
// 方式3:
let newObj = Object.create(obj)
缺点与原型链方式相同
4、寄生式继承方式
创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。
var personObj = {
running: function() {
console.log('running');
}
}
function createStudent(name) {
var stu = Object.create(personObj) // 复制
stu.name = name // 扩展
stu.studying = function() {
console.log("studying");
}
return stu
}
这种继承的优点就是对一个简单对象实现继承,缺点是没有办法实现函数的复用
5、寄生式组合继承
寄生式+构造函数调用的方式,使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。
function inheritPrototype(SubType, SuperType) {
SubType.prototype = Object.create(SuperType.prototype)
// 当然子类的prototype还需要有constructor指向子构造函数本身
Object.defineProperty(SubType.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}
// 父类
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.running = function() {
console.log("running~")
}
// 子类
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends) // 获取一份Person中的属性和方法
this.sno = sno // 可扩展自己的属性
}
// Student类还需要获取一份Person.prototype中的属性和方法
inheritPrototype(Student, Person)
var stu = new Student("why", 18, ["kobe"], 111)
stu.running()
// 打印 Student { name: 'why',age: 18,friends: [ 'kobe' ],sno: 111,}