面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念。long long ago,js是没有类的概念(ES6推出了class,但其原理还是基于原型),但是它是基于原型的语言,可以通过一些技巧来让他属于类的用法。

我们先建个最简单的对象:

var person = new Object();
person.name = "cjh";
person.age = 22;
person.sayName = function () {
//this.name => person.name
console.log(this.name);
}
//输出:cjh
person.sayName(); var person = {
name: "cjh",
age:22,
sayName:function () {
//this.name => person.name
console.log(this.name);
}
}
//输出:cjh
person.sayName();

这是JS定义对象的常用两种方式,当然两种方式的最终的结果都是一样的,在JS中有两种属性,数据属性和访问器属性:

数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值,数据属性有四个描述其行为的特性。

Configurable:表示能否通过delete删除属性从而重新定义属性,默认值是true,注意:一旦把属性定义为不可配置时(false),就不能再把它变回可以配置(true)。

Enumerable:表示能否通过for-in循环返回属性。默认值为true。(当我们for-in一个对象时,属性默认会一个个遍历出来)。

var person = {
name: "cjh",
age:22,
sayName:function () {
//this.name => person.name
console.log(this.name);
}
} for (one in person) {
console.log(one);
}
// 输出:
// name
// age
// sayName

Writeable:表示能否修改属性的值,默认为true,设成false也就是只读。

Value:顾名思义就是定义属性名是附带的值,默认是undefined,所以基本都会重写这个,不然定义属性毫无意义。

var animal = {};
Object.defineProperty(animal, "name", {
configurable: false,
value: "monkey"
});
// 输出:monkey
console.log(animal.name); //下面的语句报错,不能将false改为true
Object.defineProperty(animal, "name", {
configurable: true,
value: "monkey"
});

访问器属性

访问器属性不包含数据值:它们包含一对getter和setter函数,这和java里面的java bean一摸一样。当然他也有4个特性:

Configurable:表示能否通过delete删除属性从而重新定义属性,默认值是true

Enumerable:表示能否通过for-in循环返回属性。默认值为true。(当我们for-in一个对象时,属性默认会一个个遍历出来)。

Get:在读取属性时调用的函数,默认值为undefined

Set:在写入属性时调用的函数,默认值为undefined

注意:访问起属性不能直接定义,必须使用Object.defineProperty()定义:

var book = {
_year:2017,
now:408
};
Object.defineProperty(book, "year", {
//默认数据属性是可以直接获取,访问器属性可以在获取前加个逻辑操作
get:function () {
return this._year;
},
//默认数据属性是可以直接赋值,访问器属性可以在赋值前加个判断并且可以定义赋值操作
set:function (newValue) {
if (newValue > 2017){
this._year = newValue;
this.now++;
}
}
})
console.log('这是book定义是给的属性_year:' + book._year);
console.log('now:' + book.now);
console.log('这是用defineProperty定义的访问器属性year:' + book.year);
book.year = 2018;
console.log('year:' + book.year);
console.log('now:' + book.now);
//这时的2000没有大于先前的year:2018所以没有执行赋值
book.year = 2000;
console.log('year:' + book.year);

运行结果:

这是book定义是给的属性_year:2017
now:408
这是用defineProperty定义的访问器属性year:2017
year:2018
now:409
year:2018

注意:不一定要同时指定getter和setter,只指定getter意味着属性是只读的,尝试写入会被忽略,在严格模式下会报错。

上面的Object.defineProperty()只能一次定义一个属性,所以还有个方法定一个多个属性,

'use strict'
var book = {
};
Object.defineProperties(book, {
_year:{
value:2005,
//没有写这个,在严格模式还是会报错,虽然默认是true,
writable:true
},
now:{
value:1,
writable:true
},
  //_year是数据属性,year是访问器属性,两者可以相辅相成
year:{
get: function () {
return this._year;
}, set: function (newValue) {
if (newValue > 2017){
this._year = newValue;
this.now++;
}
}
}
});
console.log('这是book定义是给的属性_year:' + book._year);
console.log('now:' + book.now);
console.log('这是用defineProperty定义的访问器属性year:' + book.year);
book.year = 2018;
console.log('year:' + book.year);
console.log('now:' + book.now);
//这时的2000没有大于先前的year:2018所以没有执行赋值
book.year = 2000;
console.log('year:' + book.year);

Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符,这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,两种情况:如果是访问器属性对应的属性就有其访问器相应的属性,如果是数据属性也是一样。

工厂模式&&构造函数模式

工厂模式是软件工程领域一种广为人知的设计模式,解决创建多个相似对象的问题,考虑到在ES5不能创建类,所以有人就发明了一种方法,用函数来封装以特定接口创建对象的细节。

function person (name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name);
}
return o;
}
var person1 = person('cjh',22);
var person2 = person('csb',24);
//true
console.log(person1 instanceof Object);
//false
console.log(person1 instanceof person);
console.log('用工厂模式的对象类型')
//[Function: Object]
console.log(person1.constructor)
person1.sayName();
person2.sayName(); function Person (name, age) {
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name);
}
}
var person3 = new Person('ccc',22);
var person4 = new Person('bbb',24);
//true
console.log(person3 instanceof Object);
//true
console.log(person3 instanceof Person);
console.log('用构造函数模式的对象类型');
//[Function: Person]
console.log(person4.constructor)
person1.sayName();
person2.sayName();

看上面的运行结果:用工厂模式(小写的person)创建出来的对象的类型是[Function: Object],而用构造函数(大写的Person)创建出来的对象的类型是[Function: Person],这就其中区别之一,有时候我们需要知道这个对象是哪个对象实例化出来的。还注意到Person()中的代码和person()有一些不同之处:

  • 没有显式地创建对象
  • 直接将属性和方法赋给了this对象
  • 没有return语句

还应该注意到函数名Person使用的是大写字母P。按照惯例,构造函数始终都应该以一个大写字母开头,这个做法借鉴与其他OO语言(面向对象语言:C++,JAVA。。),主要是为了区别于JS中其他的函数,因为构造函数也是函数本身,只是作用不同罢了。

要创建Person新实例,必须用new操作符,以这种方式调用构造函数会经历以下4个步骤:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加新属性)
  4. 返回新对象
//new的时候做了什么
// var o1 = new Object();
// o1.[[Prototype]] = Base.prototype;
// Base.call(o1);

因为构造函数也是函数,所以调用的方式可以有很多种:

//创建出来的对象跟上面new出来的对象一样
let o = new Object();
Person.call(o, 'ddd',35);
o.sayName(); //在非严格模式下,Person里面的this会指向window,在严格模式下this没有指向,所以会报错。
Person('eee',36);
window.sayName();

总结:构造函数模式虽然比工厂模式好用,但也并非没有缺点:

function Person (name, age) {
this.name = name;
this.age = age;
// this.sayName = function(){
// console.log(this.name);
// }
this.sayName = new Function(console.log(this.name));
}

我们每次创建一个对象时,都创建了一个新的函数作用域sayName,两个不同的函数做的同一件事,这着实有点浪费资源,但有个解决方法:

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

这样确实解决了浪费资源的问题,但是这是个全局函数,任何人都可以调用它,而且当对象定义很多方法时,就要定义很多全局函数,这样毫无封装性可言,那我们前面做的都白费了,所以接下来通过原型模式来解决这个问题。

原型模式

我们创建的每个函数都有一个prototype(翻译过来:原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。还记得我开头说的JS是基于原型继承的语言。简单来说:使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

function Person () {
}
Person.prototype.name = "cjh";
Person.prototype.age = 23;
Person.prototype.sayName = function () {
console.log(this.name);
}
var person1 = new Person();
//重写了Person默认的name值
person1.name = 'csb';
person1.sayName();
var person2 = new Person();
person2.sayName();
//true person1.sayName和person2.sayName是同一个sayName()函数
console.log(person1.sayName == person2.sayName);

学过OO语言的人都知道继承的概念,里面包括重写,这里JS继承其实原理差不多,当执行到person1.name时:

解析器问:person1有name属性么,答:有,于是就读取当前属性的值,当执行到person2.name时:

解析器问:person2有name属性么,答:没有,于是向上搜索,解析器再问:person2的原型有name属性么,答:有,于是读取原型的name值。

上面这张图是来自高级JS程序设计。

这边我来解释下:

在JS中无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所以原型对象都会自动获得一个constructor(构造函数)属性,这个属性就是用来new出实例时候调用的。然而这个constructor包含一个指向prototype属性所在函数的指针,相当于绕了一大圈,形成一个闭合的链。

function Person () {
}
//[Function: Person]
console.log(Person.prototype.constructor);
Person.prototype.name = "cjh";
Person.prototype.age = 23;
Person.prototype.sayName = function () {
console.log(this.name);
}
var person1 = new Person();
//重写了Person默认的name值
person1.name = 'csb';
person1.sayName();
//[Function: Person]
console.log(person1.constructor);

//删了person1.name后,返回的就是原型中的name值了,所以这边有个函数hasOwnProperty("name")可以查看返回的是否是自己的属性,如果是原型的话就会false
delete person1.name;
person1.sayName();

看上面的运行结果可以看出来person1.constructor == Person.prototype.constructor。现在再去看上面那张图比较好理解了,这边说下,constructor是每个函数都有会属性,起初是用来标识对象类型的,但是提到检测对象类型,还是instanceof比较可靠点。

做个小扩展,JS中提供了object.hasOwnProperty("name");用来检测是否是自身的属性,我们可以建个函数用来来检测是否是原型的属性:

function hasPrototypeProperty (object, name) {
return !object.hasOwnProperty(name) && (name in object);
}

更简单的原型语法

function Person () {

}
Person.prototype = {
name: "cjh",
age:22,
sayName:function () {
console.log(this.name);
}
}
var person1 = new Person();
//重写了Person默认的name值
person1.name = 'csb';
person1.sayName();
//[Function: Object]
console.log(person1.constructor);
//true 虽然上面的构造器是Object,但是它确实是Person的实例,所以为true
console.log(person1 instanceof Person);

但是我们用第一次原型定义写法:

function Person () {
}
Person.prototype.name = "cjh";
Person.prototype.age = 23;
Person.prototype.sayName = function () {
console.log(this.name);
}
var person1 = new Person(); //[Function: Person]
console.log(person1.constructor);
//true
console.log(person1 instanceof Person);

其实结果都没差,如果你真的觉得constructor的值很重要的话,你可以重写它:

function Person () {

}
Person.prototype = {
constructor:Person,
name: "cjh",
age:22,
sayName:function () {
console.log(this.name);
}
}
var person1 = new Person();
//重写了Person默认的name值
person1.name = 'csb';
person1.sayName();
//[Function: Person]
console.log(person1.constructor);
//true
console.log(person1 instanceof Person);

还有个问题,默认原生的constructor属性是不可枚举的,也就是不可遍历的,估计会发生什么严重错误,所以不然我们枚举,但是我们重写后Enumberable就改成true,就像刚开始讲过,只要把Enumberable特性改成false就可以了,所以可以用defineProperty();

function Person () {

}
Person.prototype = {
name: "cjh",
age:22,
sayName:function () {
console.log(this.name);
}
}
var person1 = new Person();
//重写了Person默认的name值
person1.name = 'csb';
person1.sayName();
//[Function: Person]
console.log(person1.constructor);
//true 虽然上面的构造器是Object,但是它确实是Person的实例,所以为true
console.log(person1 instanceof Person); Object.defineProperty(Person.prototype, "constructor",{
enumberable:false,
value:Person
})
//遍历可枚举的属性,其中没有constructor
for (va in Person.prototype) {
console.log(va);
}

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来--即使是先创建了实例后修改原型也照样如此。

function Friend () {

}
let friend = new Friend();
Friend.prototype.sayHi = function () {
console.log('hi');
}
friend.sayHi();

尽管随时都可以为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的prototype指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型而不指向构造函数。

function Friend () {

}

Friend.prototype.sayHi = function (){
console.log('hi');
}
Friend.prototype = {
name:'cjh',
age:22,
sayName:function () {
console.log(this.name);
}
}
//{ name: 'cjh', age: 22, sayName: [Function: sayName] }
console.log(Friend.prototype);

上面定义的sayHi在哪呢?莫名其妙的没掉了?我们再看个例子,只是把定义sayHi的位置换到下面去:

function Friend () {

}

Friend.prototype = {
name:'cjh',
age:22,
sayName:function () {
console.log(this.name);
}
}
Friend.prototype.sayHi = function (){
console.log('hi');
}
// { name: 'cjh',
// age: 22,
// sayName: [Function: sayName],
// sayHi: [Function] }
console.log(Friend.prototype);

我们可以发现现在sayHi又出现了,这说明当我们用Friend.prototype.name = .....这种方式定义属性时,只是在原来的基础是上再次添加一个属性,而Friend.prototype = {};是重写整个原型,相当于新建了一个对象并且把把Firend指向新的原型,这样就出现了上面的结果。

上图来自高级JS程序设计

原型对象的问题

原型模式的问题是由其共享的本性所导致的,原型中所有属性是被很对实例共享的,这种共享对函数非常合适,对于包含引用类型值的属性来说,问题很大,看个例子:

function Person () {

}
Person.prototype = {
name:'cjh',
age:12,
friends:['aa','bb'],
sayName:function () {
console.log(this.name);
}
}; let person1 = new Person();
let person2 = new Person(); person1.friends.push('cc');
//[ 'aa', 'bb', 'cc' ]
//[ 'aa', 'bb', 'cc' ] 因为friends.push()是在原型上面添加,不是person1自己的属性,所以两个实例共享
console.log(person1.friends);
console.log(person2.friends);

这个例子说明通实例化出来的对象默认都是共享原型里面的所有值,不清楚的话,下面这个例子会更详细的介绍:

function Person () {

}
Person.prototype = {
name:'cjh',
age:12,
friends:['aa','bb'],
sayName:function () {
console.log(this.name);
}
}; let person1 = new Person();
let person2 = new Person(); //{}
//{}
console.log(person1)
console.log(person2);
person1.sayName = function () {
console.log(this.age);
}
//{ sayName: [Function] }
console.log(person1);
person1.name = 'csb';
//{ sayName: [Function], name: 'csb' }
console.log(person1);
//csb 这时name时person1自己开辟的内存,所以更改后不影响
console.log(person1.name);
//cjh 这时的name还是Person。prototypr
console.log(person2.name);
person1.friends.push('cc');
//[ 'aa', 'bb', 'cc' ]
//[ 'aa', 'bb', 'cc' ] 因为friends.push()是在原型上面添加,不是person1自己的属性,所以两个实例共享
console.log(person1.friends);
console.log(person2.friends);
person1.friends = [123,456];
// [ 123, 456 ] 现在自己新建一个属性friends就不用搜索原型的,还记得上面说的先搜索自身,没有的话再搜索原型,但person2还是用原型的friends
// [ 'aa', 'bb', 'cc' ]
console.log(person1.friends);
console.log(person2.friends);

由于原型存在属性共享的问题,也就引出了下面的解决方法。

组合使用构造函数和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享这对方法的引用,最大限度的节省了内存,何乐而不为。详情请看:JS面向对象(进阶篇

JS:面向对象(基础篇)的更多相关文章

  1. JavaScript基础精华02(函数声明,arguments对象,匿名函数,JS面向对象基础)

    函数声明 JavaScript中声明函数的方式:(无需声明返回值类型) function add(i1, i2) {             return i1 + i2;//如果不写return返回 ...

  2. 十六、python面向对象基础篇

    面向对象基础: 在了解面向对象之前,先了解下变成范式: 编程范式是一类典型的编程风格,是一种方法学 编程范式决定了程序员对程序执行的看法 oop中,程序是一系列对象的相互作用 python支持多种编程 ...

  3. Python3 面向对象(基础篇)

    面向对象 关于面向对象的标准定义网上有很多,不再讲述,现在我们来通俗点理解: 面向对象编程相对于面向过程编程和函数式编程来说,看的更长远,实现功能相对更简单. 面向对象:对象就是物体,这种编程思想就是 ...

  4. [python面向对象]--基础篇

    1.#类 #类就是一个模板,模板里可以包含多个函数,函数里实现一些功能 #定义一个类 class bar: def foo(self,agr): print(self,agr) obj = bar() ...

  5. [Js]面向对象基础

    一.什么是对象 对象是一个整体,对对外提供一些操作 二.什么是面向对象 使用对象时,只关注对象提供的功能,不关注其内部细节,比如Jquery 三.Js中面向对象的特点 1.抽象:抓住核心问题 2.封装 ...

  6. JS面向对象基础

    以往写代码仅仅是为了实现特定的功能,后期维护或别人重用的时候,困难很大. Javascript作为完全面向对象的语言,要写出最优的代码,需要理解对象是如何工作的. 1.       对象是javasc ...

  7. 第十四节 JS面向对象基础

    什么是面向对象:在不需要知道它内部结构和原理的情况下,能够有效的使用它,比如,电视.洗衣机等也可以被定义为对象 什么是对象:在Java中对象就是“类的实体化”,在JavaScript中基本相同:对象是 ...

  8. JS面向对象基础讲解(工厂模式、构造函数模式、原型模式、混合模式、动态原型模式)

    什么是面向对象?面向对象是一种思想. 面向对象可以把程序中的关键模块都视为对象, 而模块拥有属性及方法. 这样如果我们把一些属性及方法封装起来,日后使用将非常方便,也可以避免繁琐重复的工作.   工厂 ...

  9. js面向对象基础总结

     js中如何定义一个类? 定义的function就是一个构造方法也就是说是定义了一个类:用这个方法可以new新对象出来. function Person(name, age){ this.name = ...

  10. JS面向对象基础2

    根据之前看了面向对象相关的视频,按照自己的理解,整理出相关的笔记,以便自己的深入理解. javascript面向对象: 突发奇想,注意:===全等:是指既比较值,也比较类型(题外话,可忽略) 逻辑运算 ...

随机推荐

  1. 力扣算法题—149Max Points on a line

    Given n points on a 2D plane, find the maximum number of points that lie on the same straight line. ...

  2. 使用vue-cli3时怎么mock数据

    应用场景 在前后端分离的开发模式中,后端给前端提供一个接口,由前端向后端发请求,得到数据后前端进行渲染. 由于前后端开发进度的不统一,前端往往使用本地的测试数据进行数据渲染的测试. 如何配置 在vue ...

  3. leetcode.位运算.136只出现一次的元素-Java

    1. 具体题目 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次.找出那个只出现了一次的元素. 说明:你的算法应该具有线性时间复杂度. 你可以不使用额外空间来实现吗? 示例 1 ...

  4. python学习之路,2018.8.9

    python学习之路,2018.8.9, 学习是一个长期坚持的过程,加油吧,少年!

  5. [轉]Reverse a singly linked list

    Reverse a singly linked list  http://angelonotes.blogspot.tw/2011/08/reverse-singly-linked-list.html ...

  6. OOP三大特性及几大设计原则

    封装: 1.隐藏实现细节:2.恰当地公开接口:3.将接口和实现分开,增强可维护性:(实现细节改变时,使用该类的客户端程序不需要改变) 继承: 1.描述联结类的层次模型;2.通过抽象,表达共性,实现类的 ...

  7. 路过--<全世界谁倾听你>

    这首歌大概就是说男生和女生分手了男生一直忘不了女生给他带来的感觉(那种只有那个女生才能给男生带来的喜欢)就算黄昏 还是清晨 男生是男生的清晨 女生是女生的黄昏两个人没有交集了就算雨和歌都停了 风还是会 ...

  8. swat - 基于web的samba管理工具

    总览 swat [ -s smb config file ] [ -a ] 描述 此程序是 samba 套件的一部分. swat 允许 samba 管理员通过web浏览器配置复杂的 smb.conf ...

  9. JAVA基础学习-多态 对象转型 final

    一.多态的产生条件 1:继承  存在继承的类之间 2:方法重装 3:父类继承子类重装的方法 子类的对象 也是属于父类的 二:对象的转型 1:向上转型:当子类转型成父类时 例如:Animal a = n ...

  10. Nginx+Keepalived高可用集群应用实践

    Nginx+Keepalived高可用集群应用实践 1.Keepalived高可用软件 1.1 Keepalived服务的三个重要功能 1.1.1管理LVS负载均衡软件 早期的LVS软件,需要通过命令 ...