前言
本文主要回答以下几个问题:
- 1、什么是对象?
- 2、如何使用对象(创建以及访问,新增、删除、修改属性)?
- 3、原型链及继承?
什么是对象?
对象究竟是什么?什么叫面向对象编程?
- 对象(object),是面向对象(Object Oriented)中的术语,既表示客观世界问题空间(Namespace)中的某个具体的事物,又表示软件系统解空间中的基本元素。
- 在软件系统中,对象具有唯一的标识符,对象包括属性(Properties)和方法(Methods),属性就是需要记忆的信息,方法就是对象能够提供的服务。在面向对象(Object Oriented)的软件中,对象(Object)是某一个类(Class)的实例(Instance)。 —— 维基百科
1、JavaScript 对象的特征
在 JavaScript 中,对象的状态和行为其实都被抽象为了属性。 为了提高抽象能力,JavaScript 的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。
2、JavaScript 对象的两类属性
对 JavaScript 来说,属性并非只是简单的名称和值,JavaScript 用一组特征(attribute)来描述属性(property)。
数据属性
数据属性具有四个特征:
属性 | 描述 |
---|---|
value | 属性的值 |
writable | 决定属性能否被赋值 |
enumerable | 决定 for in 能否枚举该属性 |
configurable | 决定该属性能否被删除或者改变特征值 |
通常用于定义属性的代码会产生数据属性,其中的 writable、enumerable、configurable 都默认为 true。我们可以使用内置函数 getOwnPropertyDescripter 来查看。
var o = { a: 1 };
o.b = 2;
//a和b皆为数据属性
Object.getOwnPropertyDescriptor(o,"a");
// {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b");
// {value: 2, writable: true, enumerable: true, configurable: true}
访问器(getter/setter)属性 访问器属性的四个特征:
属性 | 描述 |
---|---|
getter | 函数或 undefined,在取属性值时被调用 |
setter | 函数或 undefined,在设置属性值时被调用 |
enumerable | 决定 for in 能否枚举该属性 |
configurable | 决定该属性能否被删除或者改变特征值 |
访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。
对象系统
JavaScript 中的对象分类
宿主对象:由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。 内置对象:由 JavaScript 语言提供的对象。
- 固有对象:由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
- 原生对象:可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
- 普通对象:由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。
- 宿主对象 在浏览器环境中,我们都知道全局对象是 window,window 上又有很多属性,如 document。这个全局对象 window 上的属性,一部分来自 JavaScript 语言,一部分来自浏览器环境。 JavaScript 标准中规定了全局对象属性,W3C 的各种标准中规定了 Window 对象的其它属性。
JavaScript 语言规定了全局对象的属性。
-
内置对象·固有对象 三个值:Infinity、NaN、undefined。 九个函数:eval、isFinite、isNaN、parseFloat、parseInt、decodeURI、decodeURIComponent、encodeURI、encodeURIComponent 一些构造器:Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeakSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。 四个用于当作命名空间的对象:Atomics、JSON、Math、Reflect
-
内置对象·原生对象 JavaScript 中,能够通过语言本身的构造器创建的对象称作原生对象。
通过这些构造器,我们可以用 new 运算创建新的对象。几乎所有这些构造器的能力都是无法用纯 JavaScript 代码实现的,它们也无法用 class/extend 语法来继承。这些原生对象都是为了特定能力或者性能,而设计出来的“特权对象”。
特殊行为的对象 Array:Array 的 length 属性根据最大的下标自动发生变化。 Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。 String:为了支持下标运算,String 的正整数属性访问会去字符串里查找。 Arguments:arguments 的非负整数型下标属性跟对应的变量联动。 模块的 namespace 对象:特殊的地方非常多,跟一般对象完全不一样,尽量只用于 import 吧。 类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。 bind 后的 function:跟原来的函数相关联。
如何使用对象?
[一]声明对象
创建对象的方法有两种:
- 对象字面量方法(创建对象的一种快捷方式:简化包含大量属性的对象的创建过程,即语法糖)。
- 使用new 操作符后跟Object 类型的构造函数
//对象字面量方法--创建
var obj = {
name:'foo',
method:function{console.log(this.name);
}
}
// 构造函数方法--创建
var obj = new Object();//< == > obj = {}
obj.name = 'foo';
obj.method = function(){};
几种经典的模式;工厂模式,构造函数模式,原型模式,混合构造函数/原型模式,动态原型模式,寄生构造函数模式等。
工厂模式-->用函数来封装,以特定接口创建对象的细节
function person(name,age){
var obj = new Object();//通过Object构造器创建实例对象。
obj.name = name;
obj.age = age;
obj.say = function(){
console.log(this.name);
};
return obj;
}
var person1 = person('zhangsan','18');
var person2 = person('lisi','3');
console.log(instanceOf person1)//object
console.log(instanceof person2)//object
- 解决了创建多个相似对象的问题,
- 多用于创建多个含有相同属性,和方法的对象,避免代码的重复编写;
- 没有解决对象识别的问题(
即怎么知道一个对象的类型
,无法通过instanceOf等判断区分,所有使用该模式创建的对象都是Object对象的一个实例)
构造函数模式
构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用。
var Car = function (model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
this.run =function(){
console.log(this.miles);
}
};
var baoma = new Car("Tom", 2009, 20000);
var benchi = new Car("Dudu", 2010, 5000);
- 解决对象识别的问题(即怎么知道一个对象的类型)
- 缺点:每个方法都在每个实例上重新创建了一次,(如上面例子中 baoma.sayName !== benci.sayName)
- 解决方式:提出构造函数,在全局写一个函数申明;(这种解决方式的缺点:全局函数只是局部调用,方法过多就得创建多个全局函数,没什么封装性可言);
原型模式
function Person() {}
Person.name ='tom';
Person.prototype.friends =['jerry','miqi','carry'];
Person.prototype.logName = function() {
console.log(this.name);
}
}
var person1 = new Person();
person1.logName();//'tom'
//--可以for in 访问
for(key in person1) {
console.log(key);
}
组合使用构造函数模式和原型模式
function Person(name,age) {
this.name = name;
this.age = age;
this.friends = ['ajiao','pangzi'];
}
Person.prototype =
constructor: Person,
logName: function() {
console.log(this.name);
}
}
var person1 = new Person('evansdiy','22');
var person2 = new Person('amy','21');
person1.logName();//'evansdiy'
person1.friends.push('haixao');
console.log(person2.friends.length);//3
[二] 删除对象属性
// 仅删除属性值
obj.key = undefined
// 删的是属性名和属性值
delete obj.key
[三]查看对象的属性 (读属性)
1. 查看所有属性
// 1.1 查看所有属性
let obj = {name: 'Mia', age: 18}
Object.keys(obj) // 查看自身属性名
Object.values(obj) // 查看自身属性值
Object.entries(obj) 或 obj // 查看对象
// 1.2 查看 自身属性 和 共有属性
console.dir(obj)
// 1.3 查看共有属性(不推荐)
obj.__proto__ // 不推荐
// 1.4 判断一个属性 'xxx' 是自身的还是共有的
obj.hasOwnProperty('xxx') // true就是自身的,false就是共有或不存在
// 'xxx' in obj 不能判断出这个属性是否是自身属性还是共有属性,可以验证是否在该对象中
// obj.hasOwnProperty('xxx') 可以判断出这个属性是否是自身属性
2. 查看某个属性
obj['key'] 或 obj.key
[四] 修改 / 增加对象的【自身】属性 (写属性)
1. 直接赋值
obj.name = 'foo' 或 obj['name'] = 'foo'
2. 批量赋值
let obj = {name: 'mou'}
Object.assign(obj, {name: 'Mia', age: 18, gender: 'female'})
3. 修改原型
let obj = Object.create(common) // 推荐写法
[五]对象遍历
for in
Object.keys
[六] 对象检测与识别
1、typeof
typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、symbol、object、undefined、function等7种数据类型,但不能判断null、array等
typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效
2、instanceOf
instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
弊端
- 对于基本数据类型来说,字面量方式创建出来的结果和实例方式创建的是有一定的区别的
console.log(1 instanceof Number)//false
console.log(new Number(1) instanceof Number)//true
- 只要在当前实例的原型链上,我们用其检测出来的结果都是true。在类的原型继承中,我们最后检测出来的结果未必准确。
- 不能检测null 和 undefined
3、constructor
4.Object.prototype.toString.call() Object.prototype.toString.call() 最准确最常用的方式。首先获取Object原型上的toString方法,让方法执行,让toString方法中的this指向第一个参数的值。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window是全局对象global的引用
[七] 对象继承
7.1. 原型链(很少单独使用)
function Parent(name){
this.name = name||'父类';
}
Parent.prototype.say = function(){
alert(this.name);
}
function Sub(age){
this.age = age||'21';
};
Sub.prototype = new Parent();//子类无法传递参数给超类,因为运行时已经确定了子类继承父类的信息
var oSub = new Sub('10');
oSub.say();
console.log(oSub.constructor === Parent) // true
console.log(oSub.constructor === Sub) // false
使用原型链继承主要由三个问题:
- 字面量重写原型会中断关系(需要使用constructor修正)
- 派生类原型引用基类实例,基类实例并非在派生类创建(new)时初始化,因此派生类无法给基类传递参数。
- 如果重写了派生类的原型,可能导致基类属性覆盖派生类属性,所有实例共享父类实例属性,当父类引用属性被修改时,所有都会被改。
// 修正原型链后的代码:
function Parent(){
this.name = '父类';
}
Parent.prototype.say = function(){
alert(this.name);
}
function Sub(age){
this.age = age||'21';
};
Sub.prototype = new Parent();// 子类无法传递参数给超类,因为运行时已经确定了子类继承父类的信息
Sub.prototype.constructor = Sub
var oSub = new Sub('10');
oSub.say();
console.log(oSub.constructor === Parent) // false
console.log(oSub.constructor === Sub) // true
7.2、类式继承(借用构造函数、对象冒充)
function Parent(name)
this.name = name || '父类';
}
Parent.prototype.say = function(){
alert(this.name);
}
function Sub(params){
Parent.call(this,params);
this.sex ='male';
};
var oSub = new Sub();
oSub.say(); // 抛出异常,实例oSub无say方法
优点
:
- 每个新实例都有父类构造函数的副本
- 每个实例都是重新实例化构造函数,不存在共享属性
- 可以通过Parent.call(this,params)传递参数到父类构造函数
缺点
:
- 只能继承构造函数的属性,无法继承父类上原型属性
7.3. 组合继承:(常用继承模式)
function Parent(name){
this.name = name;
this.arr = ['哥哥','妹妹','父母'];
}
Parent.prototype.say = function () {
return this.name;
};
function Sub(name,age){
Parent.call(this,name);//第二次调用
this.age = age;
}
Sub.prototype = new Parent();//第一次调用
var oSub = new Sub('子类','21');
console.log(oSub);
存在问题: 超类型在使用过程中会被调用两次;一次是创建子类型的时候,另一次是在子类型构造函数的内部
7.4. 原型式继承:
function inherits(obj){
var F = function(){};
F.prototype = obj;
return new F();
}
var Parent = {
name:'父类',
say:function(){
alert(this.name);
}
}
var oSub = inherits(Parent);// 不需要单独创建一个子类构造器来实例化子类。
oSub.say();
基类原型指向已有实例对象:基于已经有的实例对象创建对象,同时还不用创建自定义类型。
7.5. 寄生式继承:
function inherits(obj){
var F = function(){};
F.prototype = obj;
return new F();
}
function create(o){
var obj= inherits(o);
obj.run = function () {
alert('run 方法');//同样,会共享引用
};
return obj;
}
var Parent = {
name:'父类',
say:function(){
alert(this.name);
}
}
var oSub = create(Parent);
console.log(oSub);
oSub.say();
oSub.run();
封装创建的过程,隐藏实现细节。
7.6. 寄生组合式:(理想的继承实现方式)
function inherits(Parent,Sub){
var obj = Object.create(Parent.prototype);//子类原型对象
obj.constructor = Sub;
Sub.prototype = obj
}
function Parent(name){
this.name = name;
this.arr = ['哥哥','妹妹','父母'];
}
Parent.prototype.say = function () {
return this.name;
};
function Sub(name,age){
Parent.call(this,name);
this.age = age;
}
inherits(Parent,Sub)
var oSub = new Sub('子类','21');
console.log(oSub);
console.log(oSub.constructor === Sub); // true
优点(解决以下几个问题):
- 1、父类的方法可以被复用
- 2、父类构造函数内属性不被共享
- 3、子类构建实例时可以向父类传递参数
7.7 最终版本
const extend = function(protoProps,staticProps){
var Super = this
var Sub = function(){
Super.apply(this,arguments)
}
if(protoProps && protoProps.hasOwnProperty('constructor') && typeof protoProps.constructor === 'function'){
Sub = protoProps.constructor
}
// 1、继承父类原型链上,其属性不被共享
var parentProps = Object.create(Super.prototype)
parentProps.constructor = Sub
// 2、继承静态方法
Object.assign(Sub,staticProps)
// 3、切断原型链
Sub.prototype = Object.assign(parentProps,protoProps)
return Sub
}
写个实例验证下:
var Parent = function (name){
this.name = name
}
Parent.prototype.say = function(){ alert(this.name)}
Parent.extend = extend // 静态防范挂载上
var Sub = Parent.extend({
constructor:function Sub(name,age){
Parent.call(this,name)
this.age = age
},
sayHello(){
alert("hi",this.name)
}
},{
verision:"1.0"
})
var oSub = new Sub("Tim",21)
oSub.say()
console.log(Sub.version)