JS进阶(一)-JS中的面向对象
ES6中的类和对象
- 类: 在ES6中,新增加了类的概念,可以使用class关键字声明一个类,之后用这个类实例化对象
类、对象、属性、方法
-
创建类
class name { // class body }
-
通过类创建对象
// 类必须用new实例化对象 var obj = new name();
- 类constructor构造函数
constructor()
方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过 new 命令生成对象实例时 ,自动调用该方法。如果没有显示定义, 类内部会自动给我们创建一个constructor()-
语法:
class Person { constructor(name,age) {// constructor 构造方法或者构造函数 this.name = name; this.age = age; } }
-
创建实例:
var ldh = new Person('二哈', 18); console.log(ldh.name)
- 类添加方法
-
语法:
class Person { constructor(name,age) {// constructor 构造器或者构造函数 this.name = name; this.age = age; } //成员方法 say() { console.log(this.name + '你好'); } }
-
创建实例:
var ldh = new Person('刘德华', 18); ldh.say();
-
注意: 方法之间不能加逗号分隔,同时方法不需要添加 function 关键字。
-
类的继承
- 继承
-
语法:
class Father{ // 父类 } class Son extends Father { // 子类继承父类 }
-
举例:
class Father { constructor(surname) { this.surname= surname; } say() { console.log('你的姓是' + this.surname); } } class Son extends Father{ // 这样子类就继承了父类的属性和方法 } var damao= new Son('刘'); damao.say();
-
- super 关键字
- super 关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数
-
调用父类的构造函数:
class Person { // 父类 constructor(surname){ this.surname = surname; } } class Student extends Person { // 子类继承父类 constructor(surname,firstname){ super(surname); // 调用父类的constructor(surname) this.firstname = firstname; // 定义子类独有的属性 } }
- 注意:子类在构造函数中使用super, 必须放到 this 前面 (必须先调用父类的构造方法,在使用子类构造方法)
-
案例:
class Father { constructor(surname) { this.surname = surname; } saySurname() { console.log('我的姓是' + this.surname); } } class Son extends Father { // 这样子类就继承了父类的属性和方法 constructor(surname, fristname) { super(surname); // 调用父类的constructor(surname) this.fristname = fristname; } sayFristname() { console.log("我的名字是:" + this.fristname); } } var damao = new Son('刘', "德华"); damao.saySurname(); damao.sayFristname();
-
调用父类的普通函数。
class Father { say() { return '我是爸爸'; } } class Son extends Father { // 这样子类就继承了父类的属性和方法 say() { // super.say(); super 调用父类的方法 return super.say() + '的儿子'; } } var damao = new Son(); console.log(damao.say());
ES6 中的类和对象的三个注意点:
- 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象.
- 类里面的共有属性和方法一定要加this使用.
- 类里面的this指向问题.
- constructor 里面的this:指向实例对象;
- 方法里面的this:指向这个方法的调用者
-
代码举例:
<button>点击</button> <script> var that; class Star { constructor (name, age) { // 记录this到that,便于成员函数内部获取,必须放在前面 that = this; // constructor这里面的this指向的是向实例对象 this.name = name; this.age = age; // 要用this this.sing(); this.btn = document.querySelector("button"); this.btn.onclick = this.sing; } // 方法里面的this,指向该方法的调用者 sing(){ // 要用this // 当点击按钮时,这个方法的直接调用者是button,而不是当前实例对象 console.log(this.name + "唱歌"); // 点击按钮时,如何拿到当前实例的this? 搞一个全局变量记录 console.log(that.name + "唱歌"); } } let ldh = new Star("刘德华"); </script>
经典案例
- github上web练习项目JsStudyDemo中“02-JS高级”->tab选项卡demo
构造函数和原型(ES6之前的面向对象)
- 在JS面向对象中的类class是ES6之后才开始新增的,在ES6之前是通过构造函数和原型来模拟类的实现机制
- 在典型的OOP的语言中(如 Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在ES6之前,JS中并没用引入类的概念。
- ES6,全称 ECMAScript 6.0 ,2015.06 发版。但是目前浏览器的JavaScript是ES5版本,大多数高版本的浏 览器也支持 ES6,不过只实现了 ES6 的部分特性和功能。
- 在 ES6之前 ,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。
构造函数
- 构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
- 在JS中,使用构造函数时要注意以下两点:
- 构造函数用于创建某一类对象,其首字母要大写
- 构造函数要和 new 一起使用才有意义
- 成员
- JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的 this 上添 加。通过这两种方式添加的成员,就分别称为静态成员和实例成员。
- 静态成员:在构造函数本身上添加的成员称为静态成员,只能由构造函数本身来访问
- 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
- 就是构造函数内部通过this添加的成员
- 实例成员只能通过实例化的对象来访问
- 不可以通过构造函数来访问
-
代码举例
// 创建构造函数 function Star(name,age){ // 1. 实例成员:构造函数内部通过this添加的成员 // 成员属性 this.name = name; this.age = age; // 成员函数 this.sing = function(){ console.log("我会唱歌"); } } //利用构造函数创建实例对象 var ldh = new Star('刘德华',18); console.log(ldh); //实例对象调用实例成员 ldh.sing(); // 不能通过构造函数直接访问实例成员 // Star.sing(); // console.log(Star.name); // 2. 静态成员:在构造函数本身上添加 Star.sex = '男'; // 静态成员只能通过构造函数来调用 console.log(Star.sex); // 静态成员不能用实例对象来访问 // console.log(ldh.sex);
- 构造函数的问题
-
构造函数方法很好用,但是存在浪费内存的问题。
- 可以发现两个对象分别分配了一个内存用于存放sing这个成员函数,因此造成内存浪费
-
我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?
-
构造函数原型prototype
- 构造函数通过原型分配的函数是所有对象所共享的。
- JavaScript 规定,每一个构造函数都有一个 prototype 属性(对象),指向另一个对象。注意这个 prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
- 注意:这里是构造函数有一个prototype属性,那么这个构造函数的所有实例都会共享这个prototype
- 这个prototype对象的所有属性被构造函数所拥有,那么构造函数创建的实例对象可以直接访问prototype对象的所有属性
- 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
- 总结:
- 一般情况下,公共属性定义到构造函数中
- 公共方法定义到构造函数的原型对象prototype中
- 原型对象prototype作用就是实现实例对象方法共享
-
举例
function Star(name,age){ this.name = name; this.age = age; // 成员函数, // this.sing = function(){ // console.log("我会唱歌"); // } } // 成员函数直接赋值给prototype属性 Star.prototype.sing = function(){ console.log("我会唱歌"); } var ldh = new Star('刘德华',18); var zxy = new Star('刘德华',18); // 构造函数的实例对象直接访问prototype对象的成员 ldh.sing(); zxy.sing(); // 返回true console.log(ldh.sing === zxy.sing); // 可以查看每个构造函数都有一个prototype属性 console.dir(Star)
- 疑问:
- 成员方法是放在构造函数的的原型对象prototype中,那么为什么构造函数的实例对象可以直接访问原型对象prototype的成员方法呢?
- 因为实例对象原型
__proto__
实例对象原型 __proto__
- 对象都会有一个属性
__proto__
指向构造函数的 prototype 原型对象,之所以我们对象可以直接使用构造函数 prototype 原型对象的属性和方法,就是因为对象有__proto__
原型的存在。 __proto__
对象原型和原型对象 prototype 是等价的__proto__
对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性, 因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype-
举例:
// 返回true,构造函数的原型对象与实例对象的原型对象指向同一个对象 console.log(ldh.__proto__ === Star.prototype);
- 公共方法的查找规则
ldh.sing
- 首先看ldh实例对象是否有sing方法,如果有就执行
- 如果没有,就到当前实例对象的原型
__proto__
对象的成员方法中去查找sing这个方法
- 注意:
- 为了以后方便区分,我们把构造函数的原型对象prototype称作原型对象
- 把实例对象的原型对象
__proto__
称作对象原型
constructor 构造函数
- 对象原型(
__proto__
)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。 - constructor 作用:
- 记录查看该对象引用于哪个构造函数
- 它可以让原型对象重新指向原来的构造函数。
-
一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。 此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
function Star(name,age){ this.name = name; this.age = age; } // 成员函数直接赋值给prototype属性 // Star.prototype.sing = function(){ // console.log("我会唱歌"); // } // Star.prototype.movie = function() { // console.log("我会演电影"); // } // 有多个对象的方法,我们可以给原型对象采取对象形式赋值 // 如果我们修改了原来的原型对象,给原型对象赋值的是一个新对象,那么必须手动的利用constructor指回原来的构造函数 // 替换掉了Star.prototype默认指向的对象,那么它的constructor的指向肯定也会变化 // 创建一个字面常量对象赋值给prototype Star.prototype = { //作用2:手动的利用constructor这个属性指回原来的构造函数 constructor:Star, sing: function() { console.log("我会唱歌"); }, movie: function() { console.log("我会唱歌"); } } var ldh = new Star('刘德华',18); var zxy = new Star('张学友',18); // 查看原型对象、对象原型的constructor构造函数 console.log(ldh.__proto__); console.log(Star.prototype); // 作用1:查看当前对象到底指向的是哪个构造(Star)函数,原型对象、对象原型的constructor构造函数都指向构造函数本身Star console.log(ldh.__proto__.constructor); console.log(Star.prototype.constructor);
构造函数、实例、原型对象三者之间的关系
原型链
- 原型对象prototype、对象原型__proto__也是一个对象,那么这个对象也有一个__proto__属性,那么它指向什么呢?
- 它指向系统默认的Object构造函数的原型对象Object.prototype
- Object是系统默认最基本的对象构造函数
-
那么Object.prototype也是一个对象,那么它的__proto__属性指向谁呢?指向null
console.log(ldh.__proto__); console.log(Star.prototype); console.log(Star.prototype.__proto__); console.log(Star.prototype.__proto__ === Object.prototype) console.log(Object.prototype.__proto__);
JavaScript 的成员查找机制(规则)
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
- 如果还没有就查找原型对象的原型(Object的原型对象)。
- 依此类推一直找到 Object 为止(null)。
-
__proto__
对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。function Star(name,age){ this.name = name; this.age = age; } //4. Object的prototype也没有 // 返回undefined // 3. Object的prototype有 // Object.prototype.sex = '男'; // 2. 原型对象有 // Star.prototype.sex = '男'; var ldh = new Star('刘德华',18); // 1. 本身有成员 // ldh.sex = '男'; console.log(ldh.sex);
原型对象this指向
- 构造函数中的this 指向我们实例对象.
-
原型对象里面放的是方法, 这个方法里面的this 指向的是 这个方法的调用者, 也就是这个实例对象.
function Star(name,age){ this.name = name; this.age = age; } var that; Star.prototype.sing = function(){ that = this; console.log("我会唱歌"); // this指向方法的调用者ldh console.log(this); } var ldh = new Star('刘德华',18); ldh.sing(); // true console.log(that === ldh);
扩展内置对象
- 可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能
- 注意:数组和字符串内置对象不能给原型对象覆盖操作
Array.prototype = {}
(会覆盖掉系统默认的方法) ,只能是Array.prototype.xxx = function(){}
的方式。
// 扩展内置对象
// 为Array扩展一个sum方法
Array.prototype.sum = function(){
var sum = 0;
for(var i = 0;i<this.length;i++){
sum+= this[i];
}
return sum;
}
var arr = [1,2,3];
console.log(arr.sum());
// 会覆盖系统自带的方法
// Array.prototype = {
// sum: function(){
// var sum = 0;
// for(var i = 0;i<this.length;i++){
// sum+= this[i];
// }
// return sum;
// }
// }
继承
- ES6之前并没有给我们提供 extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
- call()
-
调用这个函数, 并且修改函数运行时的 this 指向
fun.call(thisArg, arg1, arg2, ...)
- thisArg :当前调用函数 this 的指向对象
- arg1,arg2:传递的其他参数
-
举例:
function fn(){ console.log('hello world !'); // 默认this指向window console.log(this); } var o = {}; // 函数调用 fn(); // 函数调用 fn.call(); // 改变this指向,指向o对象 fn.call(o);
-
- 借用构造函数继承父类型属性
- 核心原理: 通过 call() 把父类型的 this 指向子类型的 this ,这样就可以实现子类型继承父类型的属性。
-
代码举例:
// 父构造函数 function Person (name, age, sex){ this.name = name; this.age = age; this.sex = sex; } // 子构造函数 function Student (name, age, sex, score){ // 此时父类的 this 指向子类的 this,同时调用这个函数 Person.call(this,name,age,sex); this.score = score; } var s1 = new Student('zs', 18, '男', 100); console.dir(s1);
- 借用构造函数继承父类型方法
- 一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。
- 实现步骤:
- 将子类所共享的方法提取出来,让子类的
prototype 原型对象 = new 父类()
- 本质:子类原型对象等于实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象
- 将子类的 constructor 从新指向子类的构造函数
- 将子类所共享的方法提取出来,让子类的
-
注意:不能让子原型对象直接等于父原型对象
Student.prototype = Person.prototype;
- 这样的话,父子指向同一个对象,子类新增一个独有方法,则父类也会有
-
代码举例:
// 父构造函数 function Person (name, age, sex){ this.name = name; this.age = age; this.sex = sex; } // 方法 Person.prototype.eat = function(){ console.log('吃饭'); } // 子构造函数 function Student (name, age, sex, score){ // 此时父类的 this 指向子类的 this,同时调用这个函数 Person.call(this,name,age,sex); this.score = score; } // 修改问题这么一修改,子构造函数的原型对象跟父构造函数的原型对象指向同一个对象 // 那么如果子构造函数原型对象添加了一个方法,父构造函数原型对象也会有子的方法 // Student.prototype = Person.prototype; // 1. 让子类的 prototype 原型对象 = new 父类() // 子构造函数原型对象指向父类构造函数实例对象 // 如果子类构造函数新增了一个独有方法,那这个方法是添加在了父类构造函数的实例对象中,而不会影响父类的原型对象 Student.prototype = new Person(); // 2. 将子类的 constructor 从新指向子类的构造函数 // 利用对象的形式修改了原型对象,别忘了利用constructor指回原来的原型对象 Student.prototype.constructor = Student; // 子构造函数独有方法 Student.prototype.study = function(){ console.log('学习'); } var s1 = new Student('zs', 18, '男', 100); console.log(s1); console.log(Person.prototype);