1.面向对象编程思想
- 面向过程:注重解决问题的步骤,复习问题需要的每一步,实现函数依次调用
- 面向对象:是一种程序设计思想。将数据和处理数据的程序封装到对象中
- 面向对象的特性:抽象、继承、封装、多态
- 优点:提高代码的复用性和可维护性;
2.对象
JavaScript是一种基于对象的语言,几乎所有东西都是对象。
创建对象方法:
// 1.通过字面量去创建let obj1 = { name: '张三', age: 18, type: 1 };console.log(obj1);// 2.通过 new Object() 去创建let obj2 = new Object({ name: '张三', age: 18, type: 2 });console.log(obj2);// 3.通过 Object.create() 去创建,这种创建方式和前2种有区别,区别在于创建的属性会在原型上,而不是作为自身的属性let obj3 = Object.create({ name: '张三', age: 18, type: 3 });console.log(obj3); // 打印结果为 {}, 我们定义的属性在这个空对象的原型上
3.工厂模式创建对象
工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。
工厂模式主要解决了代码冗余问题,提高了代码的复用性。
// 我们想要创建很多个具有 名称、年龄、爱好的对象。如果一个个通过字面量的形式去创建会出现很多的重复代码let p1 = {name: '张三',age: 18,hobby: ()=>{console.log('我喜欢打篮球')}};let p2 = {name: '张三风',age: 19,hobby: ()=>{console.log('我喜欢打乒乓球')}};// 我们可以通过调用函数的形式,并传入对应的参数来创建对象function person(name, age, hobby){return {name,age,hobby(){console.log(hobby);}}}const zs = person('张三', 18, '我喜欢打篮球');const zsf = person('张三风', 19, '我喜欢打乒乓球');
4.构造函数
在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。
我们先来说一下 new关键字
function Person(name, age, hobby){this.name = name;this.age = age;this.hobby = function (){console.log(hobby);}console.log('Person函数被执行')}// 调用 new 关键字(实例化)后,程序做了什么// 1.会执行构造函数new Person('张三', 18, '我喜欢打篮球');// 即使没有带括号,函数也会被执行new Person;// 2.自动创建一个空对象,把空对象和this绑定起来// 3.如果构造函数没有指定的返回值,隐式返回this
构造函数其实和工厂模式写法上的区别:构造函数不需要我们自己去定义空对象和返回最终结果。
// 工厂模式function createPerson(name, age){// 需要自己去定义一个空对象const obj = {};obj.name = name;obj.age = age;// 需要自己手动返回创建的对象return obj;}// 调用方式和普通函数一样;const zs = createPerson('张三', 18);console.log(zs);// 构造函数function Person(name, age){// 构造函数最终由new关键字调用,它帮开发定义了一个空对象,并和this绑定,最终隐式返回thisthis.name = name;this.age = age;}// 调用方式,由new关键字调用函数const zsf = new Person('张三丰', 18);console.log(zsf);
实例对象
// 我们通过new 构造函数的返回结果就是一个实例对象// 构造函数中的this是指向实例对象的
静态成员
// 静态成员: 在构造函数本身上添加的属性或方法,只能由构造函数本身来访问,和实例化对象无关function Person(name, age){this.name = name;this.age = age;}// 静态成员,用来统计构造函数被实例化次数Person.callNum = 0;new Person('张三', 18);Person.callNum++;console.log(Person.callNum)
构造函数的性能问题
function Person(name, age = 18){this.name = name;this.age = age;this.say = function (){console.log('我玩中单上分快');}}const ys = new Person('亚索');const akl = new Person('阿卡丽');// 我们发现, ys 和 akl 都有一个共同的方法say,由于引用类型的原因,它们虽然看起来一样,但是却占据了不同的内存空间console.log(ys.say === akl.say); // false// 假设我们有100个实例对象,那么就会有100个占用不同内存空间的say方法,这也就造成了性能上的浪费// 其实构造函数还给我们提供了一个公共的空间: prototype,我们可以把一些重复的属性、方法放到公共的空间来解决上面提到的问题Person.prototype.say = function (){// 这个公共空间的this也是指向实例化对象的}
5.原型
js分为函数对象和普通对象,每个对象都有__proto__属性,但是只有函数对象才有prototype属性- 属性
__proto__是一个对象,它有两个属性,constructor和__proto__ - 原型对象
prototype有一个默认的constructor属性,用于记录实例是由哪个构造函数创建
let tem;function Person(name, age = 18){this.name = name;this.age = age;}Person.prototype.fn = function (){console.log('fn');tem = this;}// 1.定义在原型对象上的方法中的this指向实例化对象const ys = new Person('亚索');ys.fn();console.log(ys === tem); // true// 2.实例化对象有 __proto__ 属性,构造函数有 prototype 属性,且两者相等console.log(ys.__proto__ === Person.prototype); // true// 3.原型对象中存在constructor属性,且constructor指向创建实例对象的构造函数// 补充:constructor 可以用来做类型判断,我们可以用这个属性来判断当前实例是那个构造函数的实例化对象console.log(Person.prototype.constructor === Person);console.log(ys.constructor === Person);
看完上面原型的介绍,我们可以再来对比一下 工厂模式和构造函数的区别
- 工厂模式没有解决对象识别的问题,即创建的所有实例都是
Object类型 (不清楚是那个函数创建的) - 工厂模式没有原型,占用更多的内存空间
6.原型链
对象之间的继承关系,在 JavaScript 中是通过 prototype 对象指向父类对象,直到指向 Object 对象为止。这样就形成了一个原型指向的链条,称之为原型链。
- 当访问一个对象属性或方法时,会现在对象自身上查找属性或方法是否存在,如果存在就使用自身的属性或方法。如果不存在就去创建对象的构造函数的原型对象中查找,依次类推,直到找到为止。如果在顶层对象中还找不到,则返回
undefined。 - 原型链最顶层为
Object构造函数的prototype原型对象,给Object.prototype添加属性或方法可以被除null和undefined之外的所有数据类型对象使用。
7. JavaScript 继承
什么是继承
一个类获取另一个或者多个类的属性或者方法。继承可以使得子类具有父类的各种方法和属性。在不影响父类的前提下,减少重复的代码。
继承的原理
复制父类的方法和属性来重写子类的原型对象
法一:原型链继承
function Parent () {this.name = 'kevin';this.likes = ['游戏', '旅游']}Parent.prototype.getName = function () {console.log(this.name);}function Child () {}Child.prototype = new Parent();// child可以访问父类的构造属性和原型方法,当是不能在实例化时给福构造函数传参// 还有一个缺点: 引用类型的属性被所有实例共享,如果有多个子类实例,某个实例修改了父类的引用类型的数据,那么所有实例的该属性都会发生变化const child1 = new Child();const child2 = new Child();child1.getName(); // kevinchild1.name = 'test';console.log(child1.name, child2.name); // test, kevinchild1.likes.push('运动');console.log(child1.likes, child2.likes); // 都打印 ['游戏', '旅游', '运动']// 优点: 写法简单// 缺点1.父类使用this声明的属性被所有实例共享。 原因是实例化是父类一次性赋值到子类实例的原型上,它会将父类通过this声明的属性也赋值到子类原型上。例如在父类中一个数组值,在子类的多个实例中,无论哪一个实例去修改这个数组的值,都会影响到其他子类实例。2.创建子类实例时,无法向父类构造函数传参,不够灵活。
法二:借用构造函数(call)
function Parent () {this.names = ['kevin', 'daisy'];this.sayHello = function (){console.log('hello')}}function Child () {Parent.call(this);}const child1 = new Child();const child2 = new Child();child1.names.push('akl');console.log(child1.names); // ["kevin", "daisy", "akl"]console.log(child2.names); // ["kevin", "daisy"]// 优点:// 1.避免了引用类型的属性被所有实例共享// 2.可以在 Child 中向 Parent 传参// 缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法
法三:组合继承
这种方式是把法一和法二组合在一起使用
function Parent (name) {this.name = name;this.colors = ['red', 'blue', 'green'];}Parent.prototype.getName = function () {console.log(this.name)}function Child (name, age) {Parent.call(this, name);this.age = age;}Child.prototype = new Parent();const child1 = new Child('kevin', '18');const child2 = new Child('daisy', '20');child1.colors.push('black');console.log(child1.name); // kevinconsole.log(child1.age); // 18console.log(child1.colors); // ["red", "blue", "green", "black"]console.log(child2.name); // daisyconsole.log(child2.age); // 20console.log(child2.colors); // ["red", "blue", "green"]// 优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
法四:原型式继承
function createObj(o) {function F(){}F.prototype = o;return new F();}// 就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。// 缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。const person = {name: 'kevin',friends: ['daisy', 'kelly']}const person1 = createObj(person);const person2 = createObj(person);person1.name = 'person1';console.log(person2.name); // kevinperson1.firends.push('taylor');console.log(person2.friends); // ["daisy", "kelly", "taylor"]// 注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值。
法五:寄生式继承
function createObj (o) {const clone = object.create(o);clone.sayName = function () {console.log('hi');}return clone;}// 缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
法六:寄生组合式继承
// 组合继承最大的缺点是会调用两次父构造函数。// 一次是设置子类型实例的原型的时候:Child.prototype = new Parent();// 一次在创建子类型实例的时候:const child1 = new Child('kevin', '18');// 那么我们该如何精益求精,避免这一次重复调用呢?// 如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?function Parent (name) {this.name = name;this.colors = ['red', 'blue', 'green'];}Parent.prototype.getName = function () {console.log(this.name)}function Child (name, age) {Parent.call(this, name);this.age = age;}// 关键的三步const F = function () {};F.prototype = Parent.prototype;Child.prototype = new F();const child1 = new Child('kevin', '18');console.log(child1);
// 封装写法function object(o) {function F() {}F.prototype = o;return new F();}function prototype(child, parent) {const prototype = object(parent.prototype);prototype.constructor = child;child.prototype = prototype;}// 当我们使用的时候:prototype(Child, Parent);// 这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
