面向对象编程是将事物看成一个个对象,对象有自己的属性有自己的方法。
比如人,我们先定义一个对象模板,我们可以定义一些属性 比如,名字年龄和功能,比如走路。我们把这个叫做类。
然后帮们将具体数据传入模板,成为一个个具体的人,我们将它叫做实例。
JS 中面向对象是使用原型(prototype
)实现的。
function Person(name, age) {
this.name = name
this.age = age
this.walk = function(){}
}
Person.prototype.walk = function () {}
var bob = new Person('bob', 10)
console.log(bob.age)
其中的Person
函数叫做构造函数,构造函数一般会将第一个字母大写, 构造函数创建特定类型的对象,构造函数中没有,显式的创建对象,和返回对象,直接将属性赋值给 this
。
我们使用new
关键字创建对象实例,它会经历 4 个步骤,
- 创建一个新对象
- 将构造函数的的作用域赋给新对象
- 执行代码
- 返回新对象,实例会保存着一个
constructor
属性,该属性指向构造函数
我们也可以将walk
函数写在构造函数中this.walk=function(){}
,但是这样写的话,每新建一个实例,实例都会新建一个walk
函数,这样就浪费内存空间,我们将它放在prototype
上这样就会让所有实例共享一个walk
函数,但是如果都写了它会调用自己的walk
函数而不是共享的。
每一个函数都有一个prototype
属性,函数的prototype
对象上的属性方法,所有实例都是共享的。
prototype
对象有个constructor
属性,它指向它的构造函数。

当创建一个实例时,实例内有会有个[[Prototype]]
指针指向构造函数的原型对象,在浏览器中查看显示为__proto__
属性。
当实例访问一个属性或者调用一个方法,比如bob.walk()
,内部会首先在自身上查找这个方法,如果找到的话就完成,如果没有找到的话,就会沿着[[prototype]]
向上查找,这就是为什么prototype
上的方法都是共享,如果沿着[[prototype]]
找到头,还没找到,那么就会报错bob.walk
不是一个函数。
继承
继承主要是利用原型链,让子类的prototype等于父类的实例,也就是利用实例寻找属性和方法时,会沿着[[prototype]]
向上找。
继承就是,一个子类继承父类的代码,而不用重新编写重复的代码。比如我们要写Cat
, Dog
等类,我们发现每个类都有类似this.name = name; this.age = age
这些重复的代码,所以我们可以先写一个Animal
类,让Cat
,Dog
继承这个类,我们就不用编写重复的属性和方法了。
function Animal(name) { this.name = name; this.age = 10 }
Animal.prototype.say = function () {
console.log(this.name)
}
function Cat() { Animal.apply(this, arguments) }
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
我们用apply
改变Cat
的this
指向,让我们可以借用Animal
的构造函数,然后再让Cat
的prototype
指向一个Animal
实例,并把constructor
修改正常。
如果我们初始化一个Cat
类,然后调用say
方法,那么在内部的查找流程是:
自身 -> 沿着[[prototype]]找到Cat.prototype(它是一个Animal实例)-> 沿着Animal实例的[[prototype]]查找 -> 找到Animal.prototype(找到run方法并调用)
我们发现Cat.prototype = new Animal()
这样就会让Cat
的prototype多出name
和age
两个属性。
function Animal(name) { this.name = name; this.age = 10 }
Animal.prototype.say = function () {
console.log(this.name)
}
function Cat() { Animal.apply(this, arguments) }
function F(){}
F.prototype = Animal.prototype
Cat.prototype = new F()
Cat.prototype.constructor = Cat
我们使用了一个中间类函数F
,让它的prototype
等于父级的prototype
,那么我们查找到F.prototype
时,就自动到了Animal.prototype
上。
我们如果想知道一个属性是不是属于自身而不是来自原型链则可以使用
实例.hasOwnProperty(属性)
查看该属性是否来自本身。
Object.getOwnPropertyNames(obj)
返回所有对象本身属性名数组,无论是否能枚举
属性 in 对象
判断能否通过该对象访问该属性,无论是在本身还是原型上
如果我们想获取一个对象的prototype
,我们可以使用
Object.getPrototypeOf(obj)
方法,他返回对象的prototype
Object.setPrototypeOf(object, prototype)
方法,设置对象的prototype
还可以使用对象的__proto__
属性获取和修改对象的prototype
(不推荐)
属性描述符
在 js 中定义了只有内部才能用的特性,描述了属性的各种特性。
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
数据属性
configurable
是否能配置此属性,为false
时不能删除,而且再设置时会报错除了Writableenumerable
当且仅当该属性的enumerable
为true
时,该属性才能够出现在对象的枚举属性中value
包含了此属性的值。writable
是否能修改属性值
存取描述符
configurable
enumerable
get
读取时调用set
写入时调用
我们可以使用Object.defineProperty
方法定义或修改一个对象属性的特性。
var obj = {}
Object.defineProperty(obj, "key", {
enumerable: false, // 默认为 false
configurable: false, // 默认为 false
writable: false, // 默认为 false
value: "static" // 默认为 undefined
});
Object.defineProperty(obj, 'k', {
get: function () { // 默认为 undefined
return '123'
},
set: function (v) {
this.kk = v
} // 默认为 undefined
})
使用Object.getOwnPropertyDescriptor
可以一次定义多个属性
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
});
class
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
这样编写面向对象就更加的简单。
和类表达式一样,类声明体在严格模式下运行。构造函数是可选的。
类声明不可以提升(这与函数声明不同)。
class Person {
age = 0 // 属性除了写在构造函数中也可以写在外面。
static a = 0 // 静态属性
constructor (name) {
// 构造函数,可选(如果没有显式定义,一个空的constructor方法会被默认添加)
this.name = name
}
// 类的内部所有定义的方法,都是不可枚举的
say () { // 方法 共享函数
return this.name
}
static walk() { // 静态方法
}
}
typeof Person // "function"
Person === Person.prototype.constructor // true
使用的时候,也是直接对类使用new
命令,跟构造函数的用法完全一致,但是忘记加new
会报错。
静态属性和静态方法,是属于类的,而不是属于实例的,要使用Person.walk()
调用。
类的所有方法都定义在类的prototype
属性上面。
// 上面等同于
Person.prototype = {
constructor() {},
say() {}
};
Person.a = 0
Person.walk = function () {}
ES6 为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
Class 内部调用new.target
,返回当前 Class
与函数一样,类也可以使用表达式的形式定义。
const AA = class A {}
// 这个类的名字是A,但是A只在内部用,指代当前类。在外部,这个类只能用AA引用
const BB = class {}
let person = new class { // 立即执行的 Class
constructor(name) {
this.name = name;
}
}('张三');
Class 继承
Class 可以通过extends
关键字实现继承。
class Animal {
constructor (name) {
this.name = name
}
}
class Cat extends Animal {
constructor (...args) {
super(...args) // 调用父类的 constructor 方法
// 必须调用且放在 constructor 最前面
}
}
如果子类没有定义constructor
方法,这个方法会被默认添加。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
父类函数的静态属性和方法也会继承
super
这个关键字,既可以当作函数使用,也可以当作对象使用。
super
作为函数时,只能用在子类的构造函数之中,用在其他地方就会报错。
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
在子类普通方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类实例。
构造函数方法是不能继承原生对象的,
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
但是 class 可以继承。这样就可以构造自己的Array
子类。
可以继承了Object
,但是无法通过super
方法向父类Object
传参。这是因为 ES6 改变了Object
构造函数的行为,一旦发现Object
方法不是通过new
Object()
这种形式调用,ES6 规定Object
构造函数会忽略参数。