目录
1、面向对象初识
1.1、创建对象的方式
- 通过
new Object()
创建 - 通过字面量形式创建
1.2、对对象属性的操作
- 获取属性
- 给属性赋值
- 删除属性
1.3、对属性操作的控制——Object.defineProperty
1.3.1、语法
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
- 语法:
Object.defineProperty(obj, prop, descriptor);
可接收三个参数:
obj
要定义属性的对象prop
要定义或修改的属性的名称或Symbol
descriptor
要定义或修改的属性描述符
1.3.2、属性描述符分类
- 数据属性描述符
- 存取属性描述符
1.3.3、数据属性描述符的特征
数据属性描述符有如下四个特征:
[[configurable]]
:表示属性是否可以通过delete
属性删除,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
解释:
- 表示属性是否可以通过
delete
属性删除:即使用delete
删除该属性后,再次访问该属性依然存在。- 是否可以修改它的特性:设置
configurable
为false
后再次通过Object.defineProperty
修改该prop
的属性描述符,是不生效的,就算把configurable
再次改成true
也不会生效。
注意:
- 当我们直接在一个对象上定义某个属性时,这个属性的
[[configurable]]
为true
;- 当我们通过属性描述符定义一个属性时,这个属性的
[[configurable]]
默认为false
;
[[enumerable]]
:表示属性是否可以通过for-in
或者Object.keys()
返回该属性;
注意:
- 当我们直接在对象上定义某个属性时,这个属性的
[[enumerable]]
为true
;- 当我们通过属性描述符定义一个属性时,这个属性的
[[enumerable]]
默认为false
;
[[writable]]
:表示是否可以修改属性的值;
注意:
- 当我们直接在一个对象上定义某个属性时,这个属性的
[[writable]]
为true
;- 当我们通过属性描述符定义一个属性时,这个属性的
[[writable]]
默认为false
;
[[value]]
:属性的value
值,读取属性时会返回该值,修改属性时,会对齐进行修改;
注意:
- 当我们直接在一个对象上定义某个属性时,这个属性的
[[value]]
为我们设置的初始值;- 当我们通过属性描述符定义一个属性时,这个属性的
[[value]]
默认为undefined
;
1.3.4、存取属性描述符的特征
存取属性描述符有如下四个特征:
[[configurable]]
:表示属性是否可以通过delete
属性删除,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;[[enumerable]]
:表示属性是否可以通过for-in
或者Object.keys()
返回该属性;[[get]]
:获取属性时会执行的函数。默认为undefined
;[[set]]
:设置属性时会执行的函数。默认为undefined
;
存储属性描述符使用场景:
- 隐藏某一个私有属性,不希望直接被外界使用和赋值
- 如果我们希望截获某一个属性访问和设置值的过程时,也会使用存储属性描述符(vue2响应式实现原理)
var obj = {
name: 'zhangsan',
_address: '北京市'
}
Object.defineProperty(obj, 'address', {
enumerable: true,
configurable: true,
get: function() {
foo();
return this._address;
},
set: function(value) {
bar();
this._address = value;
}
});
console.log(obj.address);
obj.address = '上海市';
console.log(obj.address);
function foo() {
console.log('获取了address的值');
}
function bar() {
console.log('设置了address的值');
}
1.4、定义多个属性描述符——Object.defineProperties
var obj = {
// 私有属性(js里面没有严格意思上的私有属性,只是在js社区里面约定俗成的规范:以_开发表示私有)
_age: 18
};
Object.defineProperties(obj, {
name: {
configurable: true,
enumerable: true,
writable: true,
value: 'why'
},
age: {
configurable: false,
enumerable: false,
get: function() {
return this._age;
},
set: function(value) {
this._age = value;
}
},
});
console.log(obj.age);
obj.age = 20;
console.log(obj.age);
set/get的另一种写法:
var obj = {
// 私有属性(js里面没有严格意思上的私有属性,只是在js社区里面约定俗成的规范:以_开发表示私有)
_age: 18,
get age() {
return this._age;
},
set age(value) {
this._age = value;
}
};
1.5、获取某一个特定属性的属性描述符
- 语法:
Object.getOwnpropertyDescriptor(obj, prop);
1.6、获取对象所有属性的属性描述符
- 语法:
Object.getOwnpropertyDescriptors(obj);
2、Object上的方法——对对象进行限制
1、禁止对象继续添加新的属性
- 语法:
Object.preventExtensions(obj);
var obj = {
name: 'why',
age: 18
}
Object.preventExtensions(obj);
obj.height = 1.88;
// height没有添加上
console.log(obj);
2、禁止对象配置/删除里面的属性
- 语法:
Object.seal(obj);
var obj = {
name: 'why',
age: 18
}
Object.seal(obj);
delete obj.name;
// name没有被删除掉
console.log(obj);
3、让属性不可以修改
- 语法:
Object.freeze(obj);
var obj = {
name: 'why',
age: 18
}
Object.freeze(obj);
obj.name = 'mary';
// name没有被修改
console.log(obj.name);
3、创建多个对象的方案
3.1、方案一——字面量形式
3.1.1、实现
var obj1 = {
name: 'zhangsan',
age: 11
...
}
var obj2 = {
name: '李四',
age: 18
...
}
3.1.2、缺点
如果对象结构相似的话,会写大量重复的代码。
3.2、方案二——工厂模式
3.2.1、实现
function createPerson(name, age, height, address) {
// 也可通过new Object()的方式生成一个对象
var p = {};
p.name = name;
p.age = age;
p.height = height;
p.address = address;
p.eating = function() {
console.log(this.name + '在吃东西');
}
p.running = function() {
console.log(this.name + '在跑步');
}
return p;
}
var p1 = createPerson('张三', 18, 1.88, '四川省');
var p2 = createPerson('李四', 21, 1.70, '贵州省');
3.2.2、缺点
- 获取不到对象最真实的类型(都是Object类型,过于宽泛,而不是这里的确定的Person类型);
3.3、方案三——构造函数
3.3.1、认识构造函数
- 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
- 如果这么一个普通的函数被使用
new
操作符来调用了,那么这个函数就称之为是一个构造函数;
3.3.2、new操作符调用的作用
- 在内存中创建一个新的对象(空对象);
- 这个对象内部的
[[prototype]]
属性(隐式原型)会被赋值为该构造函数的prototype
属性(显式原型); - 构造函数内部的
this
,会指向创建出来的新对象; - 执行函数的内部代码(函数体代码);
- 如果构造函数没有返回非空对象,则返回创建出来的新对象;(即显式地返回
基本数据类型
或null
或undefined
时,调用构造函数仍会返回创建出来的新对象。)
3.3.3、实现
function Person(name, age, height, address) {
this.name = name;
this.age = age;
this.height = height;
this.address = address;
this.eating = function() {
console.log(this.name + '在吃东西');
}
this.running = function() {
console.log(this.name + '在跑步');
}
}
var p1 = new Person('张三', 18, 1.88, '四川省');
var p2 = new Person('李四', 21, 1.70, '贵州省');
*约定俗成的规范:构造函数的首字母一般是大写的。
3.3.4、缺点
console.log(p1.eating === p2.eating); // false
console.log(p1.running === p2.running); // false
当通过构造函数创建的对象的某个属性为函数时,每创建一个新的对象,它们都会创建新的函数对象,造成空间的浪费。
3.4、对象的原型
3.4.1、对象的原型理解
我们每个对象中都有一个[[prototype]]
,这个属性称之为对象的原型(隐式原型:直接看不到它,并且不会直接使用或者修改它)。
早期的ECMA是没有规范如何查看
[[prototype]]
的。浏览器厂商给对象中提供了一个属性[[__proto__]]
,可以查看一下这个原型对象(浏览器提供,不是规范,生产中不建议使用,用于开发环境中方便测试)。
var obj = {name: '11'}
var info = {}
console.log(obj.__proto__)
console.log(info.__proto__)
ES5之后提供的
Object.getPrototypeOf
可以获取到对象的原型
Object.getPrototypeOf(obj);
3.4.2、原型的作用
当我们从一个对象中获取某一个属性时,它会触发[[get]]
操作:
- 在当前对象中去查找对应的属性,如果找到就直接使用;
- 如果没有找到,那么会沿着它的原型链
[[prototype]]
去查找;
3.5、函数的原型
- 函数也是一个对象,因此它也有
[[prototype]]隐式原型
; - 函数因为它是一个函数,所以它还会多出来一个显式原型属性:
prototype
;
function foo() {}
var p1 = new foo();
// new操作符调用函数的第二点
console.log(p1.__proto__ === foo.prototype); // true
结论:
实例对象.__proto__ === 构造函数.prototype
- 根据原型查找对象属性举例:
function Person() {
}
var p1 = new Person();
var p2 = new Person();
// 都是为true
console.log(p1.__proto__ === Person.prototype);
console.log(p2.__proto__ === Person.prototype);
// p1.name = 'why';
// p1.__proto__.name = 'zhangsan';
// Person.prototype.name = 'lisi';
p2.__proto__.name = 'wangwu';
console.log(p1.name);
3.6、函数原型上的属性
3.6.1、自有的 constructor
属性
构造函数.prototype.constructor === 构造函数
function Person() {
}
/**
* [object Object] {
constructor: [object Object] {
configurable: true,
enumerable: false,
value: function Person() {
},
writable: true
}
}
*/
console.log(Object.getOwnPropertyDescriptors(Person.prototype));
3.6.2、自己定义属性
function Foo() {
}
// 1.添加少量的属性时
// Foo.prototype.name = 'kk';
// Foo.prototype.height = '1.88';
// Foo.prototype.running = function() {
// console.log('在跑步');
// }
// 2.为了减少重复写Foo.prototype,可以直接赋值为一个对象
// Foo.prototype = {
// name: 'kk',
// height: '1.88',
// running: function() {
// console.log('在跑步');
// }
// }
// 3.方式2写法的不足,constructor属性不存在了
// 3.1.解决方式1,手动添加上constructor属性,并让它指向构造函数。不足:constructor变得可枚举了,原生的是不可枚举的
// Foo.prototype = {
// constructor: Foo,
// name: 'kk',
// height: '1.88',
// running: function() {
// console.log('在跑步');
// }
// }
// 3.2.解决方式1的不足,通过Object.defineProperty添加(真实开发也是这样使用)
Foo.prototype = {
name: 'kk',
height: '1.88',
running: function() {
console.log('在跑步');
}
}
Object.defineProperty(Foo.prototype, 'constructor', {
enumerable: false,
configurable: true,
writable: true,
value: Foo
});
// 注意:通过第1种方式添加属性时,生成实例对象的语句可以写在属性添加前/后;第2种方式添加属性时,生成实例对象的语句必须写在属性添加后(内存指向问题)
var foo1 = new Foo();
console.log(foo1.name, foo1.height, foo1.constructor)
3.7、方案四——原型和构造函数结合(最终方案)
- 属性还是挂载到自己的实例上面,保证自己实例属性的独立。(挂载到原型上的话后面的实例属性值会覆盖前面的)
function Person(name, age) {
// 不可以这样绑定属性
Person.prototype.name = name;
Person.prototype.age = age;
}
var p1 = new Person('张三', 18);
var p2 = new Person('李四', 20);
console.log(p1.name); // 李四('张三'被覆盖)
- 把函数挂载到原型上
3.7.1、实现
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在原型上挂载函数
Person.prototype.running = function() {
console.log(this.name + '在跑步~');
}
var p1 = new Person('张三', 18);
var p2 = new Person('李四', 20);
p1.running(); // 张三在跑步~
p2.running(); // 李四在跑步~
4、面向对象中的一些概念
4.1、JavaScript中的类和对象
当我们编写如下代码的时候,我们会如何来称呼这个Person呢?
- 在JS中Person应该被称之为是一个构造函数;
- 从很多面向对象语言过来的开发者,也习惯称之为类(在ES6之前,JS中是没有类这个概念的),因为类可以帮助我们创建出来对象p1、p1;
- 如果从面向对象的编程范式角度来看,Person确实是可以称之为类的;
function Person() {
}
var p1 = new Person();
var p2 = new Person();
4.2、面向对象的三大特性
面向对象有三大特性:封装、继承、多态
- 封装:编写类的过程称之为是一个封装的过程
- 继承:1、重复利用一些代码(对代码的复用);2、继承是多态的前提;
- 多态:不同的对象在执行时表现出不同的形态
5、原型链
5.1、原型链概念
5.2、顶层原型——Object.prototype
- 该对象有原型属性,但是它的原型属性已经指向的是
null
了,也就是说该对象已经是顶层原型了; - 该对象上有很多默认的属性和方法,如:
toString()
、valueOf()
等;
var obj = {};
console.log(obj.__proto__); // [Object: null prototype] {}
console.log(Object.prototype); // [Object: null prototype] {}
console.log(obj.__proto__ === Object.prototype); //true
console.log(Object.prototype.__proto__ === null); // true
从上面的Object
原型我们可以得出一个结论:Object
是所有类的父类。
6、继承的实现方案
6.1、方案一——原型链继承
6.1.1、实现
// 父类的公共属性和方法
function Person() {
this.name = 'why';
this.friends = [];
}
Person.prototype.eating = function() {
console.log(this.name + ' eating~');
}
// 子类独有的属性和方法
function Student() {
this.sno = '111';
}
// 通过原型链实现继承
Student.prototype = new Person();
Student.prototype.studying = function() {
console.log(this.name + ' studying~');
}
var stu = new Student();
console.log(stu.name); // why
stu.eating(); // why eating~
6.1.2、缺点
- 打印stu对象,继承的属性是看不到的;
console.log(stu); // Person { sno: '111' }
- 获取父类中的引用,修改引用中的值,会互相影响;
function Person() {
this.name = 'why';
this.friends = [];
}
...
Student.prototype = new Person();
...
var stu1 = new Student();
var stu2 = new Student();
// 重点1:获取引用,修改引用中的值,会互相影响
stu1.friends.push('11'); // 原因:get操作,会沿着原型链查找属性
console.log(stu2.friends); // ['11']
// stu1.friends = ['11']; // 不会影响stu2,但是赋予新值时操作很不方便
// stu1.friends = ['11', '22'];
// console.log(stu2.friends); // []
// 重点2:直接修改对象上的属性,是给本对象添加了一个新属性
stu1.name = 'zhangsna';
console.log(stu2.name); // why
- 在前面实现类的过程中都没有传递参数;
6.2、方案二——借用构造函数
6.2.1、实现
function Person(name, friends) {
this.name = name;
this.friends = friends;
}
Person.prototype.eating = function() {
console.log(this.name + 'eating~');
}
function Student(name, friends, sno) {
Person.call(this, name, friends);
this.sno = sno;
}
Student.prototype = new Person();
Student.prototype.studying = function() {
console.log(this.name + ' ' + 'studying~');
}
var stu1 = new Student('zhangsan', ['wangwu'], 111);
var stu2 = new Student('lisi', ['marry'], 222);
console.log(stu1); // Person { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
console.log(stu2); // Person { name: 'lisi', friends: [ 'marry' ], sno: 222 }
stu1.friends.push('xiaoming');
console.log(stu2.friends); // ['marry']
6.2.2、缺点
- Person函数至少会被调用两次;
- stu原型对象上会多出一些属性,但是这些属性是没有存在的必要的;
6.3、方案三——父类原型赋值给子类原型
6.3.1、实现
function Person(name, friends) {
this.name = name;
this.friends = friends;
}
Person.prototype.eating = function() {
console.log(this.name + 'eating~');
}
function Student(name, friends, sno) {
Person.call(this, name, friends);
this.sno = sno;
}
Student.prototype = Person.prototype;
Student.prototype.studying = function() {
console.log(this.name + ' ' + 'studying~');
}
var stu1 = new Student('zhangsan', ['wangwu'], 111);
console.log(stu1); // Person { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
stu1.eating(); // zhangsaneating~
6.3.2、缺点
后续在子类原型上添加的属性,会出现在父类上。从面相对象的思想来看,这是不对的。
Student.prototype.studying = function() {
console.log(this.name + ' ' + 'studying~');
}
// { eating: [Function (anonymous)], studying: [Function (anonymous)] }
console.log(Person.prototype);
6.4、方案四——原型式继承(针对对象)
var obj = {
name:' 张三',
age: 18
};
// 目的:使当前函数返回的新对象的原型,指向传入的这个对象
// 方法1:最开始的实现方案
function createObject1(o) {
function Fun() {}
Fun.prototype = o;
return new Fun();
}
// 方法2:Object.setPrototypeOf方法出现后
function createObject2(o) {
var newObj = {};
// 设置newObj的原型指向o
Object.setPrototypeOf(newObj, o);
return newObj;
}
// 方法3:es6后。Object.create原理跟上面的代码一样,只是进行了封装
var info = Object.create(obj);
// var info = createObject1(obj);
// var info = createObject2(obj);
console.log(info.__proto__); // { name: ' 张三', age: 18 }
6.5、方案五——寄生式继承(针对对象)
相当于是原型式继承跟工厂模式的一种结合。
var pObj = {
running: function() {
console.log('running');
}
}
function createStudent(obj, name) {
var stuObj = Object.create(obj);
stuObj.name = name;
stuObj.studying = function() {
console.log('studying');
}
return stuObj;
}
var stu = createStudent(pObj, '李四');
console.log(stu); // { name: '李四', studying: [Function (anonymous)] }
console.log(stu.__proto__); // { running: [Function: running] }
6.6、方案六——寄生组合式继承(最终方案)
6.6.1、实现
function Person(name, friends) {
this.name = name;
this.friends = friends;
}
Person.prototype.eating = function() {
console.log(this.name + 'eating~');
}
function Student(name, friends, sno) {
Person.call(this, name, friends);
this.sno = sno;
}
Student.prototype = Object.create(Person.prototype);
Object.defineProperty(Student.prototype, 'constructor', {
enumerable: false,
configurable: true,
writable: true,
value: Student
});
Student.prototype.studying = function() {
console.log(this.name + ' ' + 'studying~');
}
var stu1 = new Student('zhangsan', ['wangwu'], 111);
console.log(stu1); // Student { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
stu1.eating(); // zhangsaneating~
console.log(Person.prototype); // { eating: [Function (anonymous)] }
6.6.2、优化
把实现原型继承的代码封装为一个公共方法:
// 公共方法
function inheritPrototype(SubType, SuperType) {
// 兼容问题:Object.create可以换成之前写的其他实现方法
SubType.prototype = Object.create(SuperType.prototype);
Object.defineProperty(SubType.prototype, 'constructor', {
enumerable: false,
configurable: true,
writable: true,
value: SubType
});
}
function Person(name, friends) {
this.name = name;
this.friends = friends;
}
Person.prototype.eating = function() {
console.log(this.name + 'eating~');
}
function Student(name, friends, sno) {
Person.call(this, name, friends);
this.sno = sno;
}
inheritPrototype(Student, Person);
Student.prototype.studying = function() {
console.log(this.name + ' ' + 'studying~');
}
var stu1 = new Student('zhangsan', ['wangwu'], 111);
console.log(stu1); // Student { name: 'zhangsan', friends: [ 'wangwu' ], sno: 111 }
stu1.eating(); // zhangsaneating~
console.log(Person.prototype); // { eating: [Function (anonymous)] }
7、Object上用于进行判断的方法的补充
7.1、hasOwnProperty方法
用于判断某个属性是否存在于当前对象上:
obj.hasOwnProperty(prop);
var obj = {
name: '张三',
age: 18
};
var info = Object.create(obj, {
// 补充,第二个参数:给返回的新对象上添加自己的属性
address: {
value: '北京市',
enumerable: true,
}
});
console.log(info); // { address: '北京市' }
console.log(info.__proto__); // { name: '张三', age: 18 }
console.log(info.hasOwnProperty('address')); // true
console.log(info.hasOwnProperty('name')); // false
7.2、in操作符
不管某个属性是在当前对象上,还是存在于原型链上,都返回true
:
prop in obj
var obj = {
name: '张三',
age: 18
};
var info = Object.create(obj, {
// 第二个参数:给返回的新对象上添加自己的属性
address: {
value: '北京市',
enumerable: true,
}
});
console.log('address' in info); // true
console.log('name' in info); // true
7.3、instanceof
用于检测构造函数的prototype,是否出现在某个实例对象的原型链上。
实例对象 instanceof 构造函数
function inheritPrototype(SubType, SuperType) {
// 兼容问题:Object.create可以换成之前写的其他实现方法
SubType.prototype = Object.create(SuperType.prototype);
Object.defineProperty(SubType.prototype, 'constructor', {
enumerable: false,
configurable: true,
writable: true,
value: SubType
});
}
function Person() {
}
function Student() {
}
inheritPrototype(Student, Person);
var stu = new Student();
console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true
console.log(stu instanceof Object); // true
7.4、isPrototypeOf
用于检测某个对象,是否出现在某个实例对象的原型链上。
对象.inPrototypeOf(实例对象);
function Person() {
}
var p = new Person();
console.log(Person.prototype.isPrototypeOf(p));
var obj = {
name: '李四',
age: 20
}
var info = Object.create(obj);
console.log(obj.isPrototypeOf(info));
8、对象-函数-原型之间的关系
var obj = {
name: 'why'
}
console.log(obj.__proto__);
// 对象里面是有一个__proto__对象:隐式原型对象
// Foo是一个函数,那么它会有一个显式原型对象:Foo.prototype
// Foo.prototype来自哪里?
// 答案:创建了一个函数,js引擎内部会自动生成该对象,Foo.prototype = { constructor: Foo }
// Foo是一个对象,那么它会有一个隐式原型对象:Foo.__proto__
// Foo.__proto__来自哪里?
// 答案:new Function(),Foo.__proto__ = Function.prototype
// Function.prototype = { constructor: Function }
// 等价于:var Foo = new Function()
function Foo() {
}
console.log(Foo.prototype === Foo.__proto__); // false
console.log(Foo.prototype.constructor); // [Function: Foo]
console.log(Foo.__proto__.constructor); // [Function: Function]
console.log(Function.prototype === Function.__proto__); // true(特殊之处)