Javascript是一种基于对象的语言,也就是是我们常说的【面向对象编程】,在ECMA-262中把对象定义为:“无序属性的集合,其属性可以包含基本值、对象、或者函数。
一、创建对象的方式
- 通过对象字面量的方式创建对象
constobj = {};
- 通过new object创建对象
constobj = new Object({});
- 通过构造函数创建对象
function Person(name){
this.name = name;
}
const xiaoming=new Person('小明');
- 通过Object.create()创建对象
const obj = Object.create({});
二、对象的遍历
for / in (包含继承来的属性)
const obj = {
name: '小明',
age: 20,
}
for(let key in obj) {
console.log(key); // 属性key
console.log(obj[key]); // 值
}
Object.keys(),返回key的集合
参考文章:https://zhuanlan.zhihu.com/p/40601459
Object.keys
方法,返回结果是参数对象自身的(不含继承的)所有可遍历属性的键名
注意:Object.keys运行时内部会根据属性名key的类型进行不同的排序逻辑
- key的类型是Number时,Object.keys返回值会按照key值从小到大排序
- key的类型是String或者Symbol时,Object.keys返回值会按照属性被创建的时间升序排序
const obj = {
30: '三十',
10: '十',
20: '二十'
}
const keys = Object.keys(obj);
console.log(keys); // ['10', '20', '30']
const obj2 = {
c: 'c',
a: 'a',
b: 'b'
}
const keys2 = Object.keys(obj2);
console.log(keys2); // ['c', 'a', 'b']
Object.keys被调用时的执行步骤:
- 将参数转换成Object类型的对象;
- 通过转换后的对象获得属性列表properties;
-- 属性列表properties为List类型(List类型是一个链表结构的集合) - 将属性列表properties转换为Array得到最终的结果
Object.keys()不同类型的参数运行情况说明:
- Object, 返回对象自由属性名
- Undefined ,抛出TypeError
- Null , 抛出TypeError
- Boolean ,返回空数组
- Number, 返回空数组
- Symbol ,返回空数组
-
String ,返回一个['0', '1', '2, .....]
new String()运行结果说明.png
【深拷贝 和 浅拷贝】
// 手写深拷贝函数
function deepClone(obj) {
// 1 判断是否是非应用类型或者null
if (typeof obj !== 'object' || obj == null) {
return obj
}
// 2 创建一个容器
let cloneObj = new obj.constructor(); // new Object () 或者new Array()
// 3 拿到对象的keys,给容器赋值
Object.keys(obj).forEach(key => {
return cloneObj[key] = deepClone(obj[key]); // 递归
})
// 4 返回容器
return cloneObj;
}
三、 封装
参考文档:https://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html
【封装】:所谓封装,其实就是指用构造函数的方式将多个对象的属性和方法封装到一个原型对象上去,然后再从这个原型对象上生成各个实例对象
const xialuo = {
name: '夏洛',
number: '10001',
sayHi: function() {
console.log(`我是${this.name},我的学号是${this.number}`);
}
};
const madongmei = {
name: '马冬梅',
number: '10002',
sayHi: function() {
console.log(`我是${this.name},我的学号是${this.number}`);
}
};
const qiuya= {
name: '秋雅',
number: '10003',
sayHi: function() {
console.log(`我是${this.name},我的学号是${this.number}`);
}
};
xialuo.sayHi();
madongmei.sayHi();
console.log(xialuo.sayHi === madongmei.sayHi); // false
由上述代码我们可以 看出两个问题:
- 创建多个相同类型的对象实例时,代码冗余
- 且多个对象之间没有关联性,相同的方法多次创建,浪费内存
因此,封装就显得格外重要了,
// 使用构造函数进行封装
function Student(name, number) {
this.name = name;
this.number = number;
// 写在构造函数内时,每创建一个实例化对象,都会同时创建一个sayHi方法,浪费内存
//this.sayHi = function {
//console.log(`我是${this.name},我的学号是${this.number}`);
//}
}
Student.prototype.sayHi = function() {
console.log(`我是${this.name},我的学号是${this.number}`);
}
const xialuo = new Student('夏洛', 10001);
const madongmei = new Student('马冬梅', 10002);
const qiuya = new Student('秋雅', 10003);
xialuo.sayHi();
madongmei.sayHi();
console.log(xialuo.sayHi === madongmei.sayHi); // true
四、 原型
【原型】prototype 和 __ proto __
- 每个对象都有一个隐藏属性_ proto_属性(也称为隐式原型),指向它的原型对象,该对象可以直接访问其原型对象的所有方法和属性。
- 每个构造函数都有一个prototype属性(显式原型),也指向它的原型对象
*注意: 构造函数的prototype原型对象的constructor指向构造函数本身
function Student(name, number) {
this.name = name;
this.age = number;
}
Student.prototype.sayHi = function() {
console.log(`我是${this.name},我的学号是${this.number}`);
}
/* 此处 将sayHi()写到原型链上,是为了节省内存,
若写在构造函数内,则每一次生成一个实例时,都会重新创建,浪费内存
*/
const xialuo = new Student('夏洛', '10001');
xialuo.sayHi();
原型.png
由图可以看出,实例对象的隐式原型_ proto_和 构造函数的prototype都指向同一个原型对象
五、继承
所谓继承,是指一个对象继承另一个对象的所有属性和方法。
// 构造函数的继承
function People(food){
this.food = food;
}
People.prototype.eat = function() {
console.log(`我喜欢吃${this.food}`)
}
function Student(name, number) {
this.name = name;
this.number = number;
}
const people1 = new People('肉');
// 将Student的原型对象指向People的实例,继承people1的所有属性和方法
Student.prototype = people1;
Student.prototype.constructor = Student;
Student.prototype.sayHi = function() {
console.log(`我是${this.name},我的学号是${this.number}`);
}
const xialuo = new Student('夏洛', '10001');
xialuo.sayHi();
xialuo.eat('food');
Student.prototype = people1;
Student.prototype.constructor = Student;
注意:此处是对象的重新赋值,修改Student构造函数的原型对象指向时, 会同时修改Student构造函数的原型对象上的constructor(此时的Student.prototype.constructor为Peop), 因此,需要重新指回指向Student自身.
引入class类实现继承(ES6新增语法)
直接使用构造函数实现封装和继承时,比较难以记忆及理解,因此ES6新引入class类,实现对象的封装和继承相对就简单很多了。
class People {
constructor(food) {
this.food = food;
}
eat() {
console.log(`我喜欢吃${this.food}`)
}
}
// class类 使用extends 继承自People 类
class Student extends People {
constructor(food, name, number) {
super(food);
this.name = name;
this.number = number;
}
sayHi() {
console.log(`我是${this.name},我的学号是${this.number}`);
}
}
const xialuo = new Student('肉', '夏洛', 10001);
xialuo.sayHi();
xialuo.eat('food');
使用class实现对象得封装和继承只是使用上方便了很多,实际本质上是对构造函数的再处理
六 、原型链
所谓原型链,其实就是指查找对象属性或方法时,在原型对象上一层一层向上查找的过程。
以上面继承例子中的查找eat方法来说明查找的路径:
-
当查找某个属性时,首先在当前实例
对象xialuo
上查找,有则直接使用 -
如果对象xialuo不存在eat方法,继续向
xialuo的原型对象xialuo.__proto__
(即Student.prototype
)上查找,有则直接使用 -
如果xialuo的原型对象也不存在eat方法,因此会继续向xialuo的原型对象的原型对象即
Student.prototype.__proto_
(即People.prototype
)上继续查找,有则直接使用 -
如果People.prototype上也不存在,则继续向
People.prototype.__proto_
(即Object.prototype)上继续查找, 有则直接使用
如果Object.prototype上也不存在,则继续向Object.prototype.__proto_
(即Object.prototype._proto为null)上继续查找,此时结果报错(因为null上不存在该方法)
七、关于new运算符
上面我们创建Cat的实例,直接使用new Student()就完成了,看着很简单,可以使用new 来创建一个实例时,具体都做了哪些操作呢?
const xialuo = new Student('夏洛', '10001');
// 其实她的完整执行过程应该是这样的:
function newFun() {
const obj = {};
obj.__proto__ = Student.prototype;
Student.apply(obj, arguments);
return obj;
}
const xialuo = newFun('夏洛', '10001');
xialuo.sayHi();
因此可以看出,使用new + 构造函数的方式创建实例时,它经历了以下几个步骤:
(1) 创建一个空对象;
(2)继承构造函数的原型,即将空对象的原型指向构造函数的原型 xialuo.__ proto__ = Student.prototype;
(3) 执行构造函数,为这个新对象添加属性,同时将构造函数的引用赋给 this(即this指向当前对象) ;
(4) 返回新对象。
八、常用方法和运算符
instanceof
-- instanceof 被实现为一个动态地访问原型链的过程 ,主要用来检测某个对象是否是某个对象的子类实例(即是否由原型链的某个原型对象的构造函数构建出来的)
-- 语法:object instanceof constructor
let arr1 = [1,2,3];
console.log(arr1 instanceof Arrary); // true
hasOwnProperty
-- 判断某一个属性是自有属性,还是继承来的属性
console.log(xialuo .hasOwnProperty("name")); // true
console.log(xialuo .hasOwnProperty("eat")); // false
in运算符
-- 用来判断,某个实例是否含有某个属性,不管是不是本地属性
"name" in cat1
网友评论