JavaScript面向对象编程详解

919 阅读14分钟

在学习了js高级以及es6之后,再来学习面向对象编程,之前在学习es6canvas的时候已经有接触到了这种思想,感觉还是需要在深入的学习一下,这部分内容牵扯到很多原型链部分的东西,当做复习一下还是很不错的!

ES5中的面向对象

面向对象编程(OOP)具有灵活、代码可复用、高度模块化等特点。

  1. 对象是单个实物的抽象

  2. 对象是一个容器,封装了对应的属性方法,属性是对象的状态,方法是对象的行为(完成的任务)

构造函数实例化对象

ES5中没有class类的概念,所以面向对象是基于构造函数和原型链的,

注意:构造函数的名字的第一个字母要大写

特点

  1. 函数体内使用this关键字,代表了所要生成的对象实例
  2. 生成对象,必须使用new关键字实例化
function Dog() {
	this.name = name;
	this.age = age;
}
var dog1 = new Dog('dw', 10)

如果没有使用new关键字,则结果会是undefind,原因是该函数没有返回值

instanceof的用法

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。返回trueorfalse

通过instanceof来判断当前的的对象是否是实例化出来的,如果是实例化出来的this指向实例化出来的对象,也就是这里的Person,否则作为普通函数来说当前的this指向window

function Person(name, age){
	if(this instanceof Person){
		// this指向了当前的实例,外部使用了关键字new
		this.name = name;
		this.age = age;
	}else{
		// this指向了window,外部没有使用了关键字new
		return new Person(name,age);
}

new命令内部原理

  1. 创建一个空对象
  2. 构造函数的this,继承函数原型
  3. this指向构造函数的对象实例,执行构造函数内容为新对象添加属性和方法
  4. 返回这个对象
var obj = {}//创建空对象
obj.__proto__ = Person.prototype;//继承作用域
Person.call(obj,)//改变this指向
return obj //返回对象

constructor属性

每个对象在创建时都会自动拥有一个构造函数属性constructor

constructor是对象__proto__上的一个属性(如果该对象是函数,则在其prototype上),通常指向生成这个对象的函数,也就是指向构造函数的引用

obj.constructor === Obj

使用构造函数创建对象的利弊

  1. 代码冗余
  2. 能够共享内部的属性和方法

原型对象

function Foo(){};
var foo = new Foo();

原型对象:Foo.prototype

实例对象: foo就是实例对象,每一个原型对象中都有一个__proto__,每个实例对象都有一个constructor属性,这个constructor通过继承关系继承来的,它指向了当前的构造函数Foo

构造函数:用来初始化新创建对象的函数,Foo是构造函数,自动给构造函数赋予一个属性prototype,该属性指向了实例对象的原型对象

原型对象,实例对象,构造函数三者的关系

function Foo(){}
var foo = new Foo()

foo

prototype属性的作用

继承机制:通过原型对象(prototype)实现继承

原型对象的作用,就是定义所有实例对象共享的属性和方法

Foo.prototype.name = 'ljc';

原型链

tips:所有的对象都有自己的原型对象

原型链对象的原型 => 原型的原型 => 原型的原型的原型 => null

所有的对象都继承了Object.prototype上的属性和方法

查找属性和方法的规则:js引擎会先寻找对象本身的属性和方法,如果找不到就到它的原型对象去找,如果还是找不到,就到原型的原型去找,如果直到最顶层的Object.prototype还是找不到,就会返回undefined

constructor属性注意点:一旦我们修改构造函数的原型对象,为防止引用出现问题,同时也要修改原型对象的constructor属性

创建新对象拥有数组的方法

function MyArray(){};//构造函数
MyArray.prototype = Array.prototype;//复制数组的原型对象,获得方法
MyArray.prototype.constructor = MyArray;//改变constructor指向
var arr = new MyArray();//实例化对象

创建对象的n种方式

1. 对象字面量

  1. 字面量
var Person = { name: 'ljc' }
  1. new关键字
var obj = new Object();
obj.name = 'ljc';
console.log(obj);
  1. Object.create(对象)
var a = {
	getName:function(){
		console.log('ljc');
	}
}

var b = Object.create(a);
b.getName();

缺点:代码冗余

2. 工厂模式

优点:能够创建多个类似的对象

function createObj(name,age) {
	var o = new Object();//创建对象
    //添加属性和方法
	o.name = name;
	o.age = age;
	o.sayName = function() {
		console.log(this.name);
	}
    //返回对象
	return o
}
var obj = createObj('ljc',19)//每次调用返回一个对象

缺点:所有的constructor指向都相同,没有解决对象识别问题

3. 构造函数模式

function Person(name,age){
	this.name = name;
	this.age = age;
	this.sayName = function(){
		console.log(this.name);
	}
}
var man = new Person('ljc',19);
var woman = new Person('dw',18);

通过在外部使用new关键字,将属性和方法通过this绑定到相应的对象上,解决了工厂模式的遗留问题

缺点:每个对象都会有一个sayName方法,执行的功能是一样的,但是却仍然占用了不同的内存空间,浪费了内存资源

  1. 构造函数扩展模式
function Person(name,age){
	this.name = name;
	this.age = age;
	this.sayName = sayName;
}
function sayName(){
	console.log(this.name);
}

sayName方法定义成全局函数,解决了内存浪费的问题

缺点:污染全局空间

  1. 寄生构造函数模式
function createObj(name,age) {
	var o = new Object();//创建对象
    //添加属性和方法
	o.name = name;
	o.age = age;
	o.sayName = function() {
		console.log(this.name);
	}
    //返回对象
	return o
}
var obj = new createObj('ljc',19)

结合了工厂模式和构造函数模式:创建一个函数,函数体内部实例化一个对象,并且将对象返回,在外部使用new来实例化对象

缺点:由于没有使用构造函数,所以instanceof运算符和prototype属性没有意义

  1. 稳妥构造函数模式
function Person(name){
	var a = 10;
	var obj = new Object();
	obj.sayName = function(){
		console.log(a);
		console.log(name);
	}
	return o;
}
var p1 = Person('ljc');
p1.sayName();

没有公共属性,并且它的方法也不引用thisname属于函数内部的私有属性,有点像闭包,p1被称为稳妥对象

缺点:由于没有使用构造函数,所以instanceof运算符和prototype属性没有意义

4. 原型模式

将属性方法绑定在prototype上,实现共享

注意:需要改变constructor指向

function Person(){}
Person.prototype = {
	constructor: Person,
	name: 'ljc',
	age: 19,
	friends:['dw','xy'],
	sayName:function(){
		console.log(this.name);
	}
}
var me = new Person();
var you = new Person();

每个通过Person实例化出来的对象都会继承函数的原型

缺点:当我们修改其中一个对象的属性时,另一个对象下的也会被修改

me.friends.push('jc');
console.log(you.friends);//["dw","xy","jc"]

5. 组合模式

认同度最高的一种创建自定义对象的模式

function Person(name,age){
	// 定制当前对象自己的属性
	this.name = name;
	this.age = age;
	this.friends = ['dw','xy'];
};
//定制公共的方法
Person.prototype = {
	constructor: Person,
	sayName: function(){
		console.log(this.name);
	}
}
var wo = new Person('wo', 18);
var you = new Person('you', 20);

解决了原型模式状态共享问题,将私有的属性定义在函数内部,共有的方法通过原型去实现继承引用

原型链继承的多种方式

在原型对象的所有属性和方法都能被实例所共享

原型链继承

通过重写原型对象的方式,将一个父对象的属性和方法作为子对象的原型对象的属性和方法

function Animal(){
	this.name = 'alex';
	this.colors = ['red','green','blue'];
}
Animal.prototype.getName = function(){
	return this.name;
}//添加方法
function Dog(){};
Dog.prototype = new Animal();//将animal的原型对象继承给dog
Dog.prototype.constructor = Dog;//指向dog
var d1 = new Dog();
var d2 = new Dog();

存在问题

  1. 父类中的实例属性一旦赋值给子类的原型属性,此时这些属性都属于子类的共享属性,修改一个,其他的都会被修改
  2. 实例化子类型的时候,不能向父类型的构造函数传参

借助构造函数继承

通过在子类构造函数内部调用父类构造函数,实现继承,解决了原型链继承存在的问题

使用到了call关键字,改变当前函数的this指向当前的实例对象

new的过程中会将this指向当前的实例对象,因此在构造函数内部的this指向当前的实例对象

function Animal(name){
	this.name = name;
	this.colors = ['red','green','blue'];
}
Animal.prototype.getName = function(){
	return this.name;
}
function Dog(name){
	Animal.call(this,name);
}
var d1 = new Dog('阿黄');
var d2 = new Dog('小红');

**存在问题:**父类定义的==共享方法==不能被子类所继承下来,只能继承属性

组合继承

原型链继承和借助构造函数继承结合,将2者的优点集合在一起

function Animal(name) {
	this.name = name;
	this.colors = ['red', 'green', 'blue'];
}
Animal.prototype.getName = function() {
	return this.name;
}
function Dog(name) {
	Animal.call(this, name);//借助构造函数继承
}
Dog.prototype = new Animal();//原型链继承
Dog.prototype.constructor = Dog;
var d1 = new Dog('阿黄');
var d2 = new Dog('阿红');

利用原型链继承的方式(重写原型对象),将父类的共享方法继承下来,同时在子类构造函数中调用父类构造函数,使得修改一方的值,不影响另一方的值

**存在问题:**无论在什么情况下,都要调用父类构造函数2次

寄生组合式继承

利用Object.create方法将父类的原型对象传入给子类的原型对象

将组合模式继承中的第11行代码改为以下即可

Dog.prototype = Object.create(Animal.prototype)

多重继承

利用Object.assign方法来拷贝原型对象,从而实现多重继承(混入技术Mixin)

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

Me.prototype = Object.create(Person.prototype);
Object.assign(Me.prototype, Parent.prototype);

通过这样的方式,将父类构造函数的原型对象拷贝到本身的构造函数上,同时实现了将多个父函数的原型绑定到一个子元素上

Object的静态方法

1.Object.keys()

参数是一个对象,返回是一个数组,可枚举的才返回,等同于for...in循环

let a = {
    name:'ljc',
    age:19
}
let arr = ['l','j','c'];
console.log(Object.keys(arr));// ["0", "1", "2"]
console.log(Object.keys(a));// ["name", "age"]

注意:数组也是一个对象,可以作为参数传进去,返回的是数组索引

2.Object.getOwnPropertyNames()

接收一个对象作为参数,返回了一个数组,包含了该对象自身的所有属性名,包括不可枚举的属性

let arr = ['l','j','c'];
console.log(Object.getOwnPropertyNames(arr));// ["0", "1", "2", "length"]

3.Object.getPrototypeOf()

参数是一个对象,返回该对象的原型,也是获取原型对象的标准方法

function Fn() {}
let f1 = new Fn();
console.log(Object.getPrototypeOf(f1) === Fn.prototype);//true

等同于构造函数的prototype方法

注意:空对象的原型是Object.prototype

console.log(Object.getPrototypeOf({}) === Object.prototype);//true

注意:函数的原型是Function.prototype

console.log(Object.getPrototypeOf(foo) === Function.prototype)//true

4.Object.setPrototypeOf()

接收两个参数,第一个参数是现有对象,第二个参数是原型对象,把第二个作为第一个的原型

var a = {};
var b = {
    x: 1
};
Object.setPrototypeOf(a, b);//把b作为a的原型
console.log(a.x);// 1
console.log(Object.getPrototypeOf(a))// {x: 1}

模拟new关键字

function F(){
	this.foo = 'foo';
}
var f = Object.setPrototypeOf({},F.prototype);//创建空对象,继承原型,返回对象
F.call(f);//改变this指向
console.log(f);// F {foo: "foo"}

通过Object.setPrototypeof方法可以模拟实现new内部原理

5.Object.create()

创建一个新对象,使用现有的对象(传入的参数)来提供新创建的对象的__proto__,也就是将这个参数作为这个新对象的原型

var A = {
    say :function() {
        console.log('ljc');
    }
}
var B = Object.create(A)
B.say();// ljc

该方法还有第二个参数,有点复杂 MDN链接

Object上的更多方法

1.valueOf()

返回指定对象的原始值。

// Function:返回函数本身
function foo(){}
console.log( foo.valueOf() === foo );   // true
// Array:返回数组对象本身
var array = ["ABC", true, 12, -5];
console.log(array.valueOf() === array);   // true

当遇到要预期的原始值的对象时,JavaScript会自动调用它。

我们可以根据这样的特性来自定义这个方法,从而实现我们想要的操作

var obj = new Object()
obj.valueOf = function(){
	return 2;
}
console.log(obj.valueOf() === obj);
console.log(1 + obj);// 1[object Object] ------> 3

原理valueof是属于Object原型对象的方法,Object.valueof的层级会优先于原型对象内的方法,所以会先调用我们自定义的valueof方法

2.toString()

返回一个表示该对象的字符串。

var o = new Object();
o.toString(); // returns [object Object]

覆盖默认的toString方法

function Dog(name,breed,color,sex) {
  this.name = name;
  this.color = color;
}
var theDog = new Dog("Gabby", "chocolate");
Dog.prototype.toString = function dogToString() {
 var ret = "Dog " + this.name + " is " + this.color + " " ;
 return ret;// 'Dog GAbby' is chocolate
}

注意:在每个不同的数据类型中,定义了自己的toString方法

3.toLocaleString()

返回一个该对象的字符串表示,字符串的形式与用户地区有关

这几个感觉有点说不清楚,这部分来自MDN文档

4.isPrototypeOf()

用于检测一个对象是否存在于另一个对象的原型链上

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

区别inPrototypeOf是判断这个对象是不是另一个对象的儿子,instanceof是判断这个实例对象是不是由这个构造函数创建的

5.hasOwnProperty()

方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性

o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop');  

注意:只有自身的属性才为true,继承来的属性返回false,

6.getOwnPropertyDescriptor()

通过getOwnPropertyDescriptor属性获得描述属性,只能获取自身的属性,不能获取继承的属性

writable:是否可写

enumerable:是否可枚举

configurable:是否可配置,例如删除

set:作为属性的 setter 函数,如果没有 setter 则为undefined。函数将仅接受参数赋值给该属性的新值。

get:作为该属性的 getter 函数,如果没有 getter 则为undefined。函数返回值将被用作属性的值。

var obj = {
    name:'ljc'
}
console.log(Object.getOwnPropertyDescriptor(obj,'name'));
//{value: "ljc", writable: true, enumerable: true, configurable: true}

7.propertyIsEnumerable()

返回一个布尔值,表示指定的属性是否可枚举

var arr = [1,2,3];
console.log(arr.propertyIsEnumerable('length'));//false

注意:只能判断实例对象自身的属性,不能判断继承来的属性,否则为false

8.defineProperty()

直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

传参方式

Object.defineProperty(属性所在的对象,属性名,属性描述对象)

示例

let obj = {};
Object.defineProperty(obj, "key", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "ljc"
});
console.log(obj)// {key: "ljc"}

9.defineProperties()

方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。可以一次定义多个属性

示例

var obj = Object.defineProperties({},{
	p1:{
		value:123,
		enumerable:true
	},
	p2:{
		value:"ljc",
		enumerable:false
	},
	p3:{
		get:function(){
			return this.p1 + this.p2;
		},
		enumerable:true,
		configurable:true
	}
})
console.log(obj);// {p1: 666, p2: "ljc"}
console.log(obj.p1);// 666

其中的p2属性颜色变浅,因为设置了不可枚举

注意:一旦定义了取值函数get,就不能同时定义value属性,否则会报错,也不能设置writable属性

注意:调用了该方法但有传一个空对象,所有值都会被设为false

var obj4 = Object.defineProperty({},'foo',{});

在不可写的情况下,更改属性值,不会报错,也不会更改

10.属性描述对象

几个注意点

如果enumrable设置false,通常以下三个操作不会取到该属性

  • for...in
  • Object.keys()
  • JSON.stringify()

注意:虽然不能获取自身的,但是可以获取继承来的属性


  • 如果configurable为false,属性描述对象 value,writable,enumrable,configurable都不能被修改

  • writable,true改为false会允许的

  • value属性 只要writable和 configurable有一个为true,就允许被修改

11.存取器

get方法,在获取属性时会自动调用这个方法

set方法,在修改属性时会被自动调用,需要一个参数

let obj = {
    num: 2,
    get a() {
        return '我被调用了,我的值自增' + ++this.num
    },set a(value) {
        console.log('我被改了');
    }
}
console.log(obj.a);//我被调用了,我的值自增3
obj.a = 2;//我被改了

有关面向对象的知识就到这了!


在ES6中新增了class类的关键字,以及一些相关属性,优化了先前的面向对象代码可读性低的问题,新的class写法让对象原型的写法更加清晰,更加的像面向对象编程的语法,因此class也只是一个语法糖,其所有内容都可以通过es5中对象原型方式实现