一、JavaScript的面向对象
- JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:
- JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,由key和value组成;
- key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型;
- 如果值是一个函数,那么我们可以称之为是对象的方法;
1.1. 创建对象的方式
- 1)使用构造函数创建一个空对象
var obj = new Object(); obj.name = 'yzh'; obj.age = '18'; obj.height = '1.80'; obj.eating = function() { console.log('吃东西~') }; - 2)字面量的形式创建对象
var obj = { name: 'yzh', age: '18', running: function() { console.log('在跑步!') } }
二、对属性操作的控制
- 前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的:
- 但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for-in遍历的时候被遍历出来呢?
- 想要对一个属性进行比较精准的操作控制,可以使用属性描述符;
- 通过属性描述符可以精准的添加或修改对象的属性;
- 属性描述符需要使用
Object.defineProperty来对属性进行添加或者修改;
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有 属性,并返回此对象;语法:Object.defineProperty(obj, prop, descriptor);
参数:1)
obj要定义属性的对象;2)prop要定义或修改的属性的名称;3)descriptor要定义或修改的属性描述符;返回值:被传递给函数的对象;
- 属性描述符的类型有两种:
- 数据属性;
- 存取属性;
2.1. 数据属性描述符
- 数据属性描述符四个特性:
configurable可配置的,当且仅当该属性的configurable键值为true时,可以通过delete删除属性,编辑属性。默认为false;enumerable可枚举,当且仅当该属性的enumerable键值为true时,可以通过for-in、object.keys()返回该属性。默认为false;value该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为undefined;writable可写的,当且仅当该属性的writable键值为true时,属性的值,也就是上面的value,才能被赋值运算符改变。 默认为false;
// 1.对象中原来的属性三个特性都是true: var info = { address: '深圳市', hobby: '羽毛球' } console.log(info); //{ address: '深圳市', hobby: '羽毛球' } delete info.address; console.log(info); //{ hobby: '羽毛球' } for (const key in info) { console.log(key); //hobby } console.log(Object.keys(info)); //[ 'hobby' ] info.hobby = '桀桀桀'; console.log(info); //{ hobby: '桀桀桀' } // 2.自己定义属性时三个特性都是默认值(需要手动配置): var obj = { name: "yzh", age: 18 }; Object.defineProperty(obj, "height", { enumerable: true, writable: false, value: "1.88" }); obj.height = "1.80"; delete obj.height; console.log(obj); //{ name: 'yzh', age: 18, height: '1.88' } Object.defineProperty(obj, "height", { configurable: true, enumerable: true, writable: true, value: "1.88" }); obj.height = "1.80"; console.log(obj); //{ name: 'yzh', age: 18, height: '1.80' } delete obj.height; console.log(obj); //{ name: 'yzh', age: 18 }
2.2. 存取属性描述符
configurable可配置的,同数据属性描述符一样;enumerable可枚举,同数据属性描述符一样;get属性的 getter 函数,如果没有 getter,则为undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为undefined;set属性的 setter 函数,如果没有 setter,则为undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象。 默认为undefined。var obj = { name: "yzh", age: 18, _: "哈哈" }; /** * 1.隐藏某一个私有属性(js里面是没有严格意义的私有属性),不希望直接被外界使用和赋值; * 2.如果我们希望截获一个属性它访问和设置值得过程时,也会使用存储属性描述符; * * 不使用Writable、value,同样他们相对的是不能共存的 */ Object.defineProperty(obj, "hide", { configurable: true, enumerable: true, get: function() { return this._; }, set: function(value) { this._ = value; } }); console.log(obj.hide); //哈哈 obj.hide = "嘿嘿"; console.log(obj.hide); //嘿嘿 console.log(obj); //{ name: 'yzh', age: 18, _: '嘿嘿', hide: [Getter/Setter] }
2.3. 可拥有的键值
如果一个描述符不具有
value、writable、get和set中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有value或writable和get或set键,则会产生一个异常;
2.3. 同时定义多个属性
Object.defineProperties()方法直接在一个对象上定义 多个 新的属性或修改现有属性,并返回该对象;var obj = { _age: 18, _eating: function() {} }; Object.defineProperties(obj, { name: { configurable: true, enumerable: true, writable: true, value: "yzh" }, age: { //enumerable: true, get: function() { return this._age; }, set: function(value) { this._age = value; } } }); obj._age = 20; console.log(obj); //{ _age: 20, _eating: [Function: _eating], name: 'yzh' }
🍚对象方法补充:
// 获取某一个特性属性的属性描述符
console.log(Object.getOwnPropertyDescriptor(obj, "name"));
console.log(Object.getOwnPropertyDescriptor(obj, "age"));
// 获取对象的所有属性描述符
console.log(Object.getOwnPropertyDescriptors(obj));
🈲对对象限制:
var obj = {
name: 'yzh',
age: 18
};
/*
* 1.禁止对象继续添加新的属性: preventExtensions(阻止扩展)
* 给一个对象添加新的属性会失败(在严格模式下会报错);
*/
Object.preventExtensions(obj);
obj.height = 1.88;
obj.address = "深圳市";
console.log(obj); //{ name: 'yzh', age: 18 }
/*
* 2.密封对象,不允许配置和删除属性:seal(密封、封上、关闭)
* 实际是调用 preventExtensions
* 并且将现有属性的 configurable:false
*/
// for (var key in obj) {
// Object.defineProperty(obj, key, {
// configurable: false,
// enumerable: true,
// writable: true,
// value: obj[key]
// })
// }
Object.seal(obj);
delete obj.name;
console.log(obj); //{ name: 'yzh', age: 18 }
/*
* 3.冻结对象,不允许修改现有属性:freeze(冻结、冰冻)
* 实际上是调用 seal
* 并且将现有属性的 writable: false
*/
Object.freeze(obj);
obj.name = "hzy";
console.log(obj); //{ name: 'yzh', age: 18 }
三、创建对象
3.1. 创建多个对象的方案
- 如果我们现在希望创建一系列的对象:包括ace、sabot、luffy等等,他们的信息各不相同;
- 前面我们已经试过了两种方式:
- new Object方式;
- 字面量创建的方式;
- 这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码;
var obj = { name: 'ace', age: 22, height: '1.88' } var obj3 = { name: 'sabot', age: 20, height: '1.85' } var obj4 = { name: 'luffy', age: 18, height: 1.80 }
3.2. 创建多个对象的方案 - 工厂模式
- 工厂模式其实是一种常见的设计模式;
- 通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象;
// 工厂模式: 工厂函数 function createPerson(name, age, height, address) { 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("luffy", 18, 1.78, "广州市"); var p2 = createPerson("sabo", 20, 1.88, "上海市"); var p3 = createPerson("ace", 22, 1.98, "北京市"); console.log(p1, p2, p3)
优点:通过工厂模式,我们可以快速创建大量相似对象,没有重复代码;
缺点:工厂模式创建的对象属于Object,无法区分对象类型,这也是工厂模式没有广泛使用的原因;
四、认识构造函数
- 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;
- 构造函数也是一个普通的函数,函数被使用new操作符调用了,那么这个函数就是构造函数;
4.1. new操作符调用的作用
- 如果一个函数被使用new操作符调用了,那么它会执行如下操作:
- 1)在内存中创建一个
新的对象(空对象); - 2)这个对象内部的
[[prototype]]属性会被赋值为该构造函数的prototype属性; - 3)构造函数内部的
this,会指向创建出来的新对象; - 4)执行函数的内部代码(函数体代码);
- 5)如果构造函数
没有返回非空对象,则返回创建出来的新对象;function Foo() { }; var fn = new Foo(); console.log(fn); //Foo {}
- 1)在内存中创建一个
4.2. 创建多个对象的方案 - 构造函数
- 这个构造函数可以确保我们的对象是有Person的类型的(实际是constructor的属性);
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("ace", 22, 1.98, "深圳市"); console.log(p1); var p2 = new Person("luffy", 20, 1.78, "上海市"); console.log(p2);
构造函数也是有缺点的,它在于我们需要为每个对象的函数去
创建一个函数对象实例;同时会创建出重复的函数;
五、认识原型
5.1. 对象的原型理解
- JS每个对象中都有一个 [[prototype]],这个特殊的对象属性可以称之为对象的原型(隐式原型);
- 这个特殊的对象可以指向另外一个对象;
var obj = { name: "yzh" } var info = {} console.log(obj); console.log(info); - 获取这个对象的方式:
- 1)通过对象的
__proto__属性可以获取到; - 2)通过 Object.getPrototypeOf 方法可以获取到
console.log('obj---', obj.__proto__) console.log('info---', info.__proto__) // ES5之后提供的Object.getPrototypeOf console.log(Object.getPrototypeOf(obj))
- 1)通过对象的
- 这个对象的作用在于:当我们从一个对象中获取某一个属性时, 它会触发 [[get]] 操作;
- 1)在当前对象中去查找对应的属性, 如果找到就直接使用;
- 2)如果没有找到,那么会访问对象[[prototype]]内置属性指向的对象上的属性(沿着它的原型去查找);
obj.__proto__.age = 18 obj.__proto__.height = 1.88 console.log(obj.age) //18
5.2. 函数的原型理解
- 函数也是一个对象
- 函数作为对象来说, 它也是有[[prototype]] 隐式原型(因为它是一个函数,才有了这个特殊的属性);
- 函数它因为是一个函数, 所以它还会多出来一个显示原型属性: prototype;
function foo() {
}
console.log(foo.__proto__)
console.log(foo.prototype)
var f1 = new foo()
var f2 = new foo()
console.log('f1: ', f1);
console.log('f2: ', f2);
console.log(f1.__proto__ === foo.prototype) // true
console.log(f2.__proto__ === foo.prototype) // true
5.3. 再看new操作符
-
图.png:
-
那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype;
function Person() {}; var p1 = new Person(); var p2 = new Person(); /*** * 上面的操作会进行如下: * p = {} * p.__proto__ = Person.prototype */ //所以 console.log(p1.__proto__ === Person.prototype); //true console.log(p2.__proto__ === Person.prototype); //true
5.4. 函数原型上的属性
- constructor属性:原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;
function foo() { } const f1 = new foo(); console.log(foo.prototype.constructor) // [Function: foo] console.log(foo.prototype.constructor.name) //foo console.log(p1.__proto__.constructor) // [Function: foo] console.log(p1.__proto__.constructor.name) //foo - 我们也可以添加自己的属性:
foo.prototype.name = "yzh" foo.prototype.age = 18 foo.prototype.height = 1.88 foo.prototype.eating = function() { } var f1 = new foo() console.log(f1.name, f1.age) - 如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象:
- 每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性;
- 而我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是foo构造函数了;
function foo() { } foo.prototype = { name: "yzh", age: 18, height: 1.88 } var f1 = new foo() console.log(foo.prototype.constructor) //[Function: Object] console.log(f1.name, f1.age, f1.height) //yzh 18 1.88
- 如果希望constructor指向foo,那么可以手动添加:
foo.prototype = { constructor: foo, name: "yzh", age: 18, height: 1.88 } - 真实开发中我们可以通过Object.defineProperty方式添加constructor:
Object.defineProperty(foo.prototype, "constructor", { enumerable: false, configurable: true, writable: true, value: foo })
六、创建对象 – 构造函数和原型组合
- 将函数放到Person.prototype的对象上,让所有的对象去共享这些函数;
function Person(name, age, height, address) { this.name = name; this.age = age; this.height = height; this.address = address; }; Person.prototype.eating = function() { console.log(this.name + "在吃东西"); }; Person.prototype.runing = function() { console.log(this.name + "在跑步"); }; var p1 = new Person("luffy", 18, 1.88, "深圳市"); var p2 = new Person("ACE", 22, 1.98, "上海市"); p1.eating(); //luffy在吃东西 p1.runing(); //luffy在跑步 p2.eating(); //ACE在吃东西 p2.runing(); //ACE在跑步