2022-04-07 深入JavaScript高级语法三——面向对象

目录

1、面向对象初识

1.1、创建对象的方式

  1. 通过new Object()创建
  2. 通过字面量形式创建

1.2、对对象属性的操作

  1. 获取属性
  2. 给属性赋值
  3. 删除属性

1.3、对属性操作的控制——Object.defineProperty

1.3.1、语法

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • 语法:
Object.defineProperty(obj, prop, descriptor);

可接收三个参数:

  • obj要定义属性的对象
  • prop要定义或修改的属性的名称或Symbol
  • descriptor要定义或修改的属性描述符

1.3.2、属性描述符分类

  • 数据属性描述符
  • 存取属性描述符
    在这里插入图片描述

1.3.3、数据属性描述符的特征

数据属性描述符有如下四个特征:

  • [[configurable]]:表示属性是否可以通过delete属性删除,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

解释:

  • 表示属性是否可以通过delete属性删除:即使用delete删除该属性后,再次访问该属性依然存在。
  • 是否可以修改它的特性:设置configurablefalse后再次通过Object.defineProperty修改该prop的属性描述符,是不生效的,就算把configurable再次改成true也不会生效。

注意:

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[configurable]]true
  • 当我们通过属性描述符定义一个属性时,这个属性的[[configurable]]默认为false
  • [[enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

注意:

  • 当我们直接在对象上定义某个属性时,这个属性的[[enumerable]]true
  • 当我们通过属性描述符定义一个属性时,这个属性的[[enumerable]]默认为false
  • [[writable]]:表示是否可以修改属性的值;

注意:

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[writable]]true
  • 当我们通过属性描述符定义一个属性时,这个属性的[[writable]]默认为false
  • [[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对齐进行修改;

注意:

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[value]]为我们设置的初始值;
  • 当我们通过属性描述符定义一个属性时,这个属性的[[value]]默认为undefined

1.3.4、存取属性描述符的特征

存取属性描述符有如下四个特征:

  • [[configurable]]:表示属性是否可以通过delete属性删除,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
  • [[enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;
  • [[get]]:获取属性时会执行的函数。默认为undefined
  • [[set]]:设置属性时会执行的函数。默认为undefined

存储属性描述符使用场景:

  1. 隐藏某一个私有属性,不希望直接被外界使用和赋值
  2. 如果我们希望截获某一个属性访问和设置值的过程时,也会使用存储属性描述符(vue2响应式实现原理)
var obj = {
  name: 'zhangsan',
  _address: '北京市'
}

Object.defineProperty(obj, 'address', {
  enumerable: true,
  configurable: true,
  get: function() {
    foo();
    return this._address;
  },
  set: function(value) {
    bar();
    this._address = value;
  }
});

console.log(obj.address);

obj.address = '上海市';
console.log(obj.address);

function foo() {
  console.log('获取了address的值');
}


function bar() {
  console.log('设置了address的值');
}

1.4、定义多个属性描述符——Object.defineProperties

var obj = {
  // 私有属性(js里面没有严格意思上的私有属性,只是在js社区里面约定俗成的规范:以_开发表示私有)
  _age: 18
};

Object.defineProperties(obj, {
  name: {
    configurable: true,
    enumerable: true,
    writable: true,
    value: 'why'
  },
  age: {
    configurable: false,
    enumerable: false,
    get: function() {
      return this._age;
    },
    set: function(value) {
      this._age = value;
    }
  },
});

console.log(obj.age);

obj.age = 20;
console.log(obj.age);

set/get的另一种写法:

var obj = {
  // 私有属性(js里面没有严格意思上的私有属性,只是在js社区里面约定俗成的规范:以_开发表示私有)
  _age: 18,
  get age() {
    return this._age;
  },
  set age(value) {
    this._age = value;
  }
};

1.5、获取某一个特定属性的属性描述符

  • 语法:
Object.getOwnpropertyDescriptor(obj, prop);

1.6、获取对象所有属性的属性描述符

  • 语法:
Object.getOwnpropertyDescriptors(obj);

2、Object上的方法——对对象进行限制

1、禁止对象继续添加新的属性

  • 语法:
Object.preventExtensions(obj);
var obj = {
  name: 'why',
  age: 18
}

Object.preventExtensions(obj);

obj.height = 1.88;

// height没有添加上
console.log(obj);

2、禁止对象配置/删除里面的属性

  • 语法:
Object.seal(obj);
var obj = {
  name: 'why',
  age: 18
}

Object.seal(obj);

delete obj.name;
// name没有被删除掉
console.log(obj);

3、让属性不可以修改

  • 语法:
Object.freeze(obj);
var obj = {
  name: 'why',
  age: 18
}

Object.freeze(obj);

obj.name = 'mary';
// name没有被修改
console.log(obj.name);

3、创建多个对象的方案

3.1、方案一——字面量形式

3.1.1、实现

var obj1 = {
  name: 'zhangsan',
  age: 11
  ...
}

var obj2 = {
  name: '李四',
  age: 18
  ...
}

3.1.2、缺点

如果对象结构相似的话,会写大量重复的代码。

3.2、方案二——工厂模式

3.2.1、实现

function createPerson(name, age, height, address) {
  // 也可通过new Object()的方式生成一个对象
  var p = {};
  p.name = name;
  p.age = age;
  p.height = height;
  p.address = address;
  
  p.eating = function() {
    console.log(this.name + '在吃东西');
  }
  
  p.running = function() {
    console.log(this.name + '在跑步');
  }
  
  return p;
}

var p1 = createPerson('张三', 18, 1.88, '四川省');
var p2 = createPerson('李四', 21, 1.70, '贵州省');

3.2.2、缺点

  • 获取不到对象最真实的类型(都是Object类型,过于宽泛,而不是这里的确定的Person类型);

3.3、方案三——构造函数

3.3.1、认识构造函数

  • 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
  • 如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;

3.3.2、new操作符调用的作用

  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性(隐式原型)会被赋值为该构造函数的prototype属性(显式原型);
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行函数的内部代码(函数体代码);
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;(即显式地返回基本数据类型nullundefined时,调用构造函数仍会返回创建出来的新对象。)

3.3.3、实现

function Person(name, age, height, address) {
  this.name = name;
  this.age = age;
  this.height = height;
  this.address = address;
  
  this.eating = function() {
    console.log(this.name + '在吃东西');
  }
  
  this.running = function() {
    console.log(this.name + '在跑步');
  }
}

var p1 = new Person('张三', 18, 1.88, '四川省');
var p2 = new Person('李四', 21, 1.70, '贵州省');

*约定俗成的规范:构造函数的首字母一般是大写的。

3.3.4、缺点

console.log(p1.eating === p2.eating); // false
console.log(p1.running === p2.running); // false

当通过构造函数创建的对象的某个属性为函数时,每创建一个新的对象,它们都会创建新的函数对象,造成空间的浪费。

3.4、对象的原型

3.4.1、对象的原型理解

我们每个对象中都有一个[[prototype]],这个属性称之为对象的原型(隐式原型:直接看不到它,并且不会直接使用或者修改它)。

早期的ECMA是没有规范如何查看[[prototype]]的。浏览器厂商给对象中提供了一个属性[[__proto__]],可以查看一下这个原型对象(浏览器提供,不是规范,生产中不建议使用,用于开发环境中方便测试)。

var obj = {name: '11'}
var info = {}

console.log(obj.__proto__)
console.log(info.__proto__)

ES5之后提供的Object.getPrototypeOf可以获取到对象的原型

Object.getPrototypeOf(obj);

3.4.2、原型的作用

当我们从一个对象中获取某一个属性时,它会触发[[get]]操作:

  1. 在当前对象中去查找对应的属性,如果找到就直接使用;
  2. 如果没有找到,那么会沿着它的原型链[[prototype]]去查找;

3.5、函数的原型

  • 函数也是一个对象,因此它也有[[prototype]]隐式原型
  • 函数因为它是一个函数,所以它还会多出来一个显式原型属性:prototype
function foo() {}

var p1 = new foo();
// new操作符调用函数的第二点
console.log(p1.__proto__ === foo.prototype); // true

在这里插入图片描述
结论:

实例对象.__proto__ === 构造函数.prototype
  • 根据原型查找对象属性举例:
function Person() {
  
}

var p1 = new Person();
var p2 = new Person();

// 都是为true
console.log(p1.__proto__ === Person.prototype);
console.log(p2.__proto__ === Person.prototype);

// p1.name = 'why';
// p1.__proto__.name = 'zhangsan';
// Person.prototype.name = 'lisi';
p2.__proto__.name = 'wangwu';

console.log(p1.name);

3.6、函数原型上的属性

3.6.1、自有的 constructor 属性

构造函数.prototype.constructor === 构造函数
function Person() {
  
}

/**
 * [object Object] {
  constructor: [object Object] {
    configurable: true,
    enumerable: false,
    value: function Person() {
    
    },
    writable: true
  }
}
 */
console.log(Object.getOwnPropertyDescriptors(Person.prototype));

3.6.2、自己定义属性

function Foo() {

}

// 1.添加少量的属性时
// Foo.prototype.name = 'kk';
// Foo.prototype.height = '1.88';
// Foo.prototype.running = function() {
//     console.log('在跑步');
// }

// 2.为了减少重复写Foo.prototype,可以直接赋值为一个对象
// Foo.prototype = {
//     name: 'kk',
//     height: '1.88',
//     running: function() {
//         console.log('在跑步');
//     }
// }

// 3.方式2写法的不足,constructor属性不存在了
// 3.1.解决方式1,手动添加上constructor属性,并让它指向构造函数。不足:constructor变得可枚举了,原生的是不可枚举的
// Foo.prototype = {
//     constructor: Foo,
//     name: 'kk',
//     height: '1.88',
//     running: function() {
//         console.log('在跑步');
//     }
// }

// 3.2.解决方式1的不足,通过Object.defineProperty添加(真实开发也是这样使用)
Foo.prototype = {
    name: 'kk',
    height: '1.88',
    running: function() {
        console.log('在跑步');
    }
}

Object.defineProperty(Foo.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Foo
});

// 注意:通过第1种方式添加属性时,生成实例对象的语句可以写在属性添加前/后;第2种方式添加属性时,生成实例对象的语句必须写在属性添加后(内存指向问题)
var foo1 = new Foo();

console.log(foo1.name, foo1.height, foo1.constructor)

在这里插入图片描述

3.7、方案四——原型和构造函数结合(最终方案)

  • 属性还是挂载到自己的实例上面,保证自己实例属性的独立。(挂载到原型上的话后面的实例属性值会覆盖前面的)
function Person(name, age) {
    // 不可以这样绑定属性
    Person.prototype.name = name;
    Person.prototype.age = age;
}

var p1 = new Person('张三', 18);
var p2 = new Person('李四', 20);

console.log(p1.name); // 李四('张三'被覆盖)
  • 把函数挂载到原型上

3.7.1、实现

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 在原型上挂载函数
Person.prototype.running = function() {
    console.log(this.name + '在跑步~');
}

var p1 = new Person('张三', 18);
var p2 = new Person('李四', 20);

p1.running(); // 张三在跑步~
p2.running(); // 李四在跑步~

4、面向对象中的一些概念

4.1、JavaScript中的类和对象

当我们编写如下代码的时候,我们会如何来称呼这个Person呢?

  • 在JS中Person应该被称之为是一个构造函数;
  • 从很多面向对象语言过来的开发者,也习惯称之为类(在ES6之前,JS中是没有类这个概念的),因为类可以帮助我们创建出来对象p1、p1;
  • 如果从面向对象的编程范式角度来看,Person确实是可以称之为类的;
function Person() {

}

var p1 = new Person();
var p2 = new Person();

4.2、面向对象的三大特性

面向对象有三大特性:封装、继承、多态

  • 封装:编写类的过程称之为是一个封装的过程
  • 继承:1、重复利用一些代码(对代码的复用);2、继承是多态的前提;
  • 多态:不同的对象在执行时表现出不同的形态

5、原型链

5.1、原型链概念

在这里插入图片描述

5.2、顶层原型——Object.prototype

  • 该对象有原型属性,但是它的原型属性已经指向的是null了,也就是说该对象已经是顶层原型了;
  • 该对象上有很多默认的属性和方法,如:toString()valueOf()等;
var obj = {};

console.log(obj.__proto__); // [Object: null prototype] {}
console.log(Object.prototype); // [Object: null prototype] {}
console.log(obj.__proto__ === Object.prototype); //true

console.log(Object.prototype.__proto__ === null); // true

从上面的Object原型我们可以得出一个结论:Object是所有类的父类。

6、继承的实现方案

6.1、方案一——原型链继承

6.1.1、实现

// 父类的公共属性和方法
function Person() {
    this.name = 'why';
    this.friends = [];
}

Person.prototype.eating = function() {
    console.log(this.name + ' eating~');
}

// 子类独有的属性和方法
function Student() {
    this.sno = '111';
}

// 通过原型链实现继承
Student.prototype = new Person();

Student.prototype.studying = function() {
    console.log(this.name + ' studying~');
}

var stu = new Student();
console.log(stu.name); // why
stu.eating(); // why eating~

内存图

6.1.2、缺点

  1. 打印stu对象,继承的属性是看不到的;
console.log(stu); // Person { sno: '111' }
  1. 获取父类中的引用,修改引用中的值,会互相影响;
function Person() {
    this.name = 'why';
    this.friends = [];
}

...

Student.prototype = new Person();

...

var stu1 = new Student();
var stu2 = new Student();

// 重点1:获取引用,修改引用中的值,会互相影响
stu1.friends.push('11'); // 原因:get操作,会沿着原型链查找属性
console.log(stu2.friends); // ['11']

// stu1.friends = ['11']; // 不会影响stu2,但是赋予新值时操作很不方便
// stu1.friends = ['11', '22'];
// console.log(stu2.friends); // []

// 重点2:直接修改对象上的属性,是给本对象添加了一个新属性
stu1.name = 'zhangsna';
console.log(stu2.name); // why
  1. 在前面实现类的过程中都没有传递参数;

6.2、方案二——借用构造函数

6.2.1、实现

function Person(name, friends) {
    this.name = name;
    this.friends = friends;
}

Person.prototype.eating = function() {
    console.log(this.name + 'eating~');
}

function Student(name, friends, sno) {
    Person.call(this, name, friends);
    this.sno = sno;
}

Student.prototype = new Person();

Student.prototype.studying = function() {
    console.log(this.name + ' ' + 'studying~');
}

var stu1 = new Student('zhangsan', ['wangwu'], 111);
var stu2 = new Student('lisi', ['marry'], 222);
console.log(stu1); // Person { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
console.log(stu2); // Person { name: 'lisi', friends: [ 'marry' ], sno: 222 }

stu1.friends.push('xiaoming');
console.log(stu2.friends); // ['marry']

在这里插入图片描述

6.2.2、缺点

  1. Person函数至少会被调用两次;
  2. stu原型对象上会多出一些属性,但是这些属性是没有存在的必要的;

6.3、方案三——父类原型赋值给子类原型

6.3.1、实现

function Person(name, friends) {
    this.name = name;
    this.friends = friends;
}

Person.prototype.eating = function() {
    console.log(this.name + 'eating~');
}

function Student(name, friends, sno) {
    Person.call(this, name, friends);
    this.sno = sno;
}

Student.prototype = Person.prototype;

Student.prototype.studying = function() {
    console.log(this.name + ' ' + 'studying~');
}

var stu1 = new Student('zhangsan', ['wangwu'], 111);
console.log(stu1); // Person { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
stu1.eating(); // zhangsaneating~

6.3.2、缺点

后续在子类原型上添加的属性,会出现在父类上。从面相对象的思想来看,这是不对的。

Student.prototype.studying = function() {
    console.log(this.name + ' ' + 'studying~');
}
// { eating: [Function (anonymous)], studying: [Function (anonymous)] }
console.log(Person.prototype); 

6.4、方案四——原型式继承(针对对象)

原型式继承函数

var obj = {
    name:' 张三',
    age: 18
};

// 目的:使当前函数返回的新对象的原型,指向传入的这个对象
// 方法1:最开始的实现方案
function createObject1(o) {
    function Fun() {}
    Fun.prototype = o;
    return new Fun();
}

// 方法2:Object.setPrototypeOf方法出现后
function createObject2(o) {
    var newObj = {};
    // 设置newObj的原型指向o
    Object.setPrototypeOf(newObj, o);
    return newObj;
}

// 方法3:es6后。Object.create原理跟上面的代码一样,只是进行了封装
var info = Object.create(obj);

// var info = createObject1(obj);
// var info = createObject2(obj);
console.log(info.__proto__); // { name: ' 张三', age: 18 }

6.5、方案五——寄生式继承(针对对象)

相当于是原型式继承工厂模式的一种结合。
寄生式继承函数

var pObj = {
    running: function() {
        console.log('running');
    }
}

function createStudent(obj, name) {
    var stuObj = Object.create(obj);
    stuObj.name = name;
    stuObj.studying = function() {
        console.log('studying');
    }
    return stuObj;
}

var stu = createStudent(pObj, '李四');
console.log(stu); // { name: '李四', studying: [Function (anonymous)] }
console.log(stu.__proto__); // { running: [Function: running] }

6.6、方案六——寄生组合式继承(最终方案)

6.6.1、实现

function Person(name, friends) {
    this.name = name;
    this.friends = friends;
}

Person.prototype.eating = function() {
    console.log(this.name + 'eating~');
}

function Student(name, friends, sno) {
    Person.call(this, name, friends);
    this.sno = sno;
}

Student.prototype = Object.create(Person.prototype);
Object.defineProperty(Student.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Student
});

Student.prototype.studying = function() {
    console.log(this.name + ' ' + 'studying~');
}

var stu1 = new Student('zhangsan', ['wangwu'], 111);
console.log(stu1); // Student { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
stu1.eating(); // zhangsaneating~
console.log(Person.prototype); // { eating: [Function (anonymous)] }

6.6.2、优化

把实现原型继承的代码封装为一个公共方法:

// 公共方法
function inheritPrototype(SubType, SuperType) {
    // 兼容问题:Object.create可以换成之前写的其他实现方法
    SubType.prototype = Object.create(SuperType.prototype);
    Object.defineProperty(SubType.prototype, 'constructor', {
        enumerable: false,
        configurable: true,
        writable: true,
        value: SubType
    });
}

function Person(name, friends) {
    this.name = name;
    this.friends = friends;
}

Person.prototype.eating = function() {
    console.log(this.name + 'eating~');
}

function Student(name, friends, sno) {
    Person.call(this, name, friends);
    this.sno = sno;
}

inheritPrototype(Student, Person);

Student.prototype.studying = function() {
    console.log(this.name + ' ' + 'studying~');
}

var stu1 = new Student('zhangsan', ['wangwu'], 111);
console.log(stu1); // Student { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
stu1.eating(); // zhangsaneating~
console.log(Person.prototype); // { eating: [Function (anonymous)] }

7、Object上用于进行判断的方法的补充

7.1、hasOwnProperty方法

用于判断某个属性是否存在于当前对象上:

obj.hasOwnProperty(prop);
var obj = {
    name: '张三',
    age: 18
};

var info = Object.create(obj, {
    // 补充,第二个参数:给返回的新对象上添加自己的属性
    address: {
        value: '北京市',
        enumerable: true,
    }
});

console.log(info); // { address: '北京市' }
console.log(info.__proto__); // { name: '张三', age: 18 }

console.log(info.hasOwnProperty('address')); // true
console.log(info.hasOwnProperty('name')); // false

7.2、in操作符

不管某个属性是在当前对象上,还是存在于原型链上,都返回true

prop in obj
var obj = {
    name: '张三',
    age: 18
};

var info = Object.create(obj, {
    // 第二个参数:给返回的新对象上添加自己的属性
    address: {
        value: '北京市',
        enumerable: true,
    }
});

console.log('address' in info); // true
console.log('name' in info); // true

7.3、instanceof

用于检测构造函数的prototype,是否出现在某个实例对象的原型链上。

实例对象 instanceof 构造函数
function inheritPrototype(SubType, SuperType) {
    // 兼容问题:Object.create可以换成之前写的其他实现方法
    SubType.prototype = Object.create(SuperType.prototype);
    Object.defineProperty(SubType.prototype, 'constructor', {
        enumerable: false,
        configurable: true,
        writable: true,
        value: SubType
    });
}

function Person() {

}

function Student() {

}

inheritPrototype(Student, Person);

var stu = new Student();

console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true
console.log(stu instanceof Object); // true

7.4、isPrototypeOf

用于检测某个对象,是否出现在某个实例对象的原型链上。

对象.inPrototypeOf(实例对象);
function Person() {

}
var p = new Person();
console.log(Person.prototype.isPrototypeOf(p));


var obj = {
    name: '李四',
    age: 20
}
var info = Object.create(obj);
console.log(obj.isPrototypeOf(info));

8、对象-函数-原型之间的关系

var obj = {
	name: 'why'
}
console.log(obj.__proto__);

// 对象里面是有一个__proto__对象:隐式原型对象

// Foo是一个函数,那么它会有一个显式原型对象:Foo.prototype
// Foo.prototype来自哪里?
// 答案:创建了一个函数,js引擎内部会自动生成该对象,Foo.prototype = { constructor: Foo }

// Foo是一个对象,那么它会有一个隐式原型对象:Foo.__proto__
// Foo.__proto__来自哪里?
// 答案:new Function(),Foo.__proto__ = Function.prototype
//  Function.prototype = { constructor: Function }

// 等价于:var Foo = new Function()
function Foo() {

}

console.log(Foo.prototype === Foo.__proto__); // false
console.log(Foo.prototype.constructor); // [Function: Foo]
console.log(Foo.__proto__.constructor); // [Function: Function]
console.log(Function.prototype === Function.__proto__); // true(特殊之处)

原型继承关系

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值