ES5
原型
获取对象的原型:
const obj = {
name: '张三',
age: 18
}
// 浏览提共的获取对象原型的方法
console.log(obj.__proto__);
// 官方提供的获取对象原型的方法
console.log(Object.getPrototypeOf(obj));
作用
获取某个对象的属性的查找方式如下:
- 优先从当前对象查找,找到即返回,否则执行步骤2
- 沿着原型链查找,找到即返回,直到返回null
示例:我们在Object原型上设置了sex属性,对于一个新创建的对象obj上是没有这个sex属性的,但是当我们查找obj.sex的时候却返回了value“男”,这就是原型的作用之一。
Object.prototype.sex = '男'
const obj = {
name: '张三',
age: 18
}
// 浏览提共的获取对象原型的方法
console.log(obj.__proto__);
// 官方提供的获取对象原型的方法
console.log(Object.getPrototypeOf(obj));
console.log(obj.sex)
console.log(obj.id)
函数的原型prototype
var obj = {};
function test() {}
// 1.将函数看成一个普通的对象时,它是具有__proto__(隐式原型)
console.log('obj.__proto__:', obj.__proto__)
console.log('test.__proto__:',test.__proto__)
// 2.将函数看成一个函数时,它是具备prototype
// 作用:用来构建对象时,给对象设置隐式原型的
console.log('test.prototype:', test.prototype)
// 对象是没有prototype的
// console.log('obj.prototype:', obj.prototype)
作用
function Student (name, age) {
this.name = name;
this.age = age;
this.running = function(){
console.log(`${this.name}在跑步`)
}
}
Student.prototype.singing = function() {
console.log(`${this.name}在唱歌`);
}
var s1 = new Student('张三', 18);
var s2 = new Student('李四', 18);
console.log('他们的running是不是同一个?', s1.running === s2.running);
console.log('他们的singing是不是同一个?', s1.singing === s2.singing);
问题:我们可以从上得出,创建多个student对象就会创建多个running(),但很明显,running()是可以共用的。
解决办法:通过显示原型prototype进行设置,不管创建多少个student对象,他们的singing()都指向同一个,这就是prototype的作用之一。
函数的prototype是一个对象。
constructor
constructor是原型对象上的属性,指向当前的函数对象。
举例:
对于下面的Student函数。
Student的原型:Student.prototype
constructor: Student.prototype.contructor
关系:Student.prototype.contructor 指向Student
function Student (name, age) {
this.name = name;
this.age = age;
}
var studentPrototype = Student.prototype;
console.log('studentPrototype:', studentPrototype)
console.log('studentPrototype.constructor:', studentPrototype.constructor);
console.log(studentPrototype.constructor === Student)
面向对象的特性
面向对象有三大特性:封装、继承、多态。
- 封装:将属性和方法封装到一个类中,称之为封装;
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
- 多态:不同的对象在执行时表现出不同的姿态。
原型链继承
目前student的原型是p对象,而p对象的原型是Person默认的原型。
注意:步骤4和5是不可以调整顺序的,否则会有问题。(在下面的示例中,会报错studying is not a function)
// 1.定义父类构造函数
function Person (name) {
this.name = name;
}
// 2.父类原型上添加内容
Person.prototype.running = function () {
console.log(`${this.name} is running`);
}
// 3.定义子类构造函数
function Student (age) {
this.age = age;
}
// 4.创建一个父类的实例对象(new Person()),用这个实例对象作为子类的原型对象
var p = new Person('张三');
Student.prototype = p;
// 5.在子类原型上添加内容
Student.prototype.studying = function() {
console.log(`${this.name} is studying`)
}
var s = new Student(18);
console.log(s.name);
console.log(s.running());
console.log(s.studying());
原型链继承的弊端
某些属性其实是保存在p对象上的
- 第一,我们通过打印对象是看不到这个属性的;
- 第二,这个属性会被多个对象共享。如果这个对象是一个引用类型,那么就会造成问题;
- 第三,不能给Person传递参数(让每个stu有自己的属性)。因为这个对象是一次性创建的(没办法定制化)。
组合继承
组合继承是借用构造函数来实现继承的目的。
Person.call(this, name)这里调用Person()构造函数,此时函数里面的this指向的是Student的示例对象。
// 1.定义父类构造函数
function Person (name) {
this.name = name;
}
// 2.父类原型上添加内容
Person.prototype.running = function () {
console.log(`${this.name} is running`);
}
// 3.定义子类构造函数
function Student (name, age) {
// 借用构造函数继承
Person.call(this, name)
this.age = age;
}
var p = new Person('李四', 12);
Student.prototype = p;
// 4.在子类原型上添加内容
Student.prototype.studying = function() {
console.log(`${this.name} is studying`)
}
var s = new Student('张三',18);
console.log(s.name);
console.log(s.running());
console.log(s.studying());
存在的问题
组合继承最大的问题是无论在什么情况下,都会调用两次父类构造函数。
- 一次在创建子类原型的时候;
- 另一次在子类构造函数内部(每次创建子类实例的时候)
所有子类实例事实上会拥有两份父类的属性:
- 一份在当前的实例里面(person本身),另一份在子类对应的原型对象中(person.__proto__里面);但是不会出现问题,默认一定是访问实例本身这部分。
寄生组合继承
// 创建对象的过程
function createObject (obj) {
function F() {};
F.prototype = obj;
return new F();
}
// 将SubType和SuperType联系在一起
// 寄生式函数
function inherit (SubType, SuperType) {
SubType.prototype = createObject(SuperType);
Object.defineProperty(SubType.prototype, 'constructor', {
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}
ES6
原型继承关系
class类的定义
定义方法:类声明式和类表达式
// 第一种
class XXX {
}
// 第二种
var yyy = class XXX {
}
示例:
// ES5类的声明
function Student (name, age) {
this.name = name;
this.age = age;
}
Student.prototype.running = function () {
console.log(`${this.name} is running`);
}
// ES6类的声明
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(`${this.name} is running`);
}
}
var p = new Person('张三', 18);
var p1 = new Person('张三ds', 18);
console.log(p.name)
p.running();
console.log(p.__proto__ === Person.prototype) // true
console.log(p.running == p1.running) // true
类的访问器编写方式:
class Person {
constructor(name, age) {
this._name = name;
this._age = age;
}
set name(value) {
console.log('设置新的name')
this._name = value;
}
get name() {
console.log('返回name')
return this._name;
}
running() {
console.log(`${this._name} is running`);
}
}
var p = new Person('张三', 18);
// console.log(p.age) undefined
// console.log(p._age) 18 不建议
console.log(p.name)
p.name = '李四';
new一个对象时发生的事情
- 在内存中创建一个新的对象(空对象);
- 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
- 构造函数内部的this,会指向创建出来的新对象;
- 执行构造函数的内部代码(函数体代码);
- 如果构造函数没有返回非空对象,则返回创建出来的新对象。
类的静态方法
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义:
class Person {
constructor(name, age) {
this._name = name;
this._age = age;
}
static sayHello() {
console.log('这是静态方法');
}
static sVal = '静态属性';
}
console.log(Person.sayHello());
console.log(Person.sVal);
extends继承
super关键字
- 执行super.methodName(…)来调用父类方法
- 执行super(…)来调用父类constructor(只能在子类的constructor中使用)
class Person {
constructor(name) {
this.name = name;
}
running () {
console.log(`${this.name} is running`);
}
}
class Student extends Person{
constructor(name, age) {
super(name);
this.age = age;
}
}
const stu = new Student('张三', 18);
console.log(stu.name); // 张三
stu.running(); // 张三 is running
Proxy
我们先看一下Object.defineProperty如何监听对象的属性:
var obj = {
name: '中',
age: 1
}
// 监听所有的属性:遍历所有的属性,对每一个属性使用defineProperty
const keys = Object.keys(obj);
for (const key of keys) {
let val = obj[key];
Object.defineProperty(obj, key, {
set: function(newVal) {
console.log(`${key}设置了新的值${newVal}`);
value = newVal;
},
get: function () {
console.log(`返回${key}的值${value}`);
return value;
},
})
}
obj.name = '李四';
console.log(obj.name);
问题:
Object.defineProperty对于新增属性、删除属性等操作是无能为力的。
解决方法:
如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(proxy);之后对该对象的所有操作,都可以通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作。
MDN文档
const p = new Proxy(target, handler)
参数
- target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
- handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
var obj = {
name: '中',
age: 1
}
const p = new Proxy(obj, {
get: function (target, property, receiver) {
// target:目标对象
// property: 被获取的属性名
// receiver: Proxy 或者继承 Proxy 的对象
console.log(`监听${target}对象的${property}`)
return target[property];
},
set: function (target, property, value, receiver) {
// value: 新属性值
console.log(`监听${target}对象设置的${property}属性的值${value}`)
target[property] = value;
}
})
p.name;
p.name = '张三'
Reflect
作用:
主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;比如:Reflect.getPrototypeOf(target)类似于Object.getPrototypeOf();、Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()。
为什么需要reflect?
早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面。但是Object作为一个构造函数,这些操作实际上放到它身上并不合适。因此,ES6新增了Reflect,就把这些操作集中到了Reflect对象上了。最后,在使用Proxy时,可以做到不操作原对象。
Reflect和Object的比较
var obj = {
name: '中',
age: 1
}
const p = new Proxy(obj, {
get: function (target, property, receiver) {
// target:目标对象
// property: 被获取的属性名
// receiver: Proxy 或者继承 Proxy 的对象
console.log(`监听${target}对象的${property}`)
return target[property];
},
set: function (target, property, value, receiver) {
// target[property] = value;
// 1.代理对象的目的:不再直接操作原对象
// 2.Reflect.set方法有返回Boolean值,可以判断set是否成功
// 3.receiver就是外层的Proxy对象
// 4.Reflect.set/get最后一个参数,可以决定对象访问器setter/getter的this指向
const isSuc = Reflect.set(target, property, value);
if (!isSuc) {
console.log('赋值失败')
}
}
})
p.name;
p.name = '张三'