JavaScript高级 & ES6
一、ES6中的类和对象
1.1 JavaScript中的对象
在JavaScript语言中,对象是一组无序的相关属性和方法的集合,所有的事物都是对象,例如字符串、数值、数组、函数等。
对象是由属性和方法组成的:
- 属性:事物的一些特性
- 方法:事物的行为或功能
1.2 类 class
ES6新增了类的概念,可以使用class关键字声明一个类,之后以这个类为模板来实例化对象。
1.3 创建类
语法:
class Person{
constructor(name,age){ //构造函数
this.name = name;
this.age = age;
}
//在类中所有的函数都可以不用写function
say(){ //类的共有方法
console.log('人可以说话')
}
}
创建实例:
//之前定义变量使用var,但是var中存在一些问题,例如作用域提升
//因此现在更推荐使用let声明一个变量
let xx = new Person('张三',12) //使用new关键字创建对象
1.4 类的构造函数 constructor
constructor()方法是类的构造函数,用于传递参数并返回一个实例化对象。该方法不需要显式调用,在new对象的时候会自动执行。如果没有显式定义构造函数,系统就会自动给我们创建一个constructor()。
1.5 类的继承 extends
继承,就是将父类中的公有的方法和属性继承过来达到复用的目的,这个也符合现实世界的特征。例如,猫和狗都是动物,而且他们都是哺乳动物,都能够吃饭、睡觉。
示例:
class Father{ //父类
constructor(){...}
money(){...}
}
class Son extends Father{ //子类
//这样就可以将父类中的方法和属性继承过来
//虽然现在子类中并没有出现money()方法,但是一旦子类继承父类,就会自动拥有父类的非私有方法和属性
}
1.6 super 关键字
super关键字用于访问父类中的函数,不仅可以调用父类构造函数,也可以调用父类中的普通函数。
示例:
class Father{ //父类
constructor(name,age){...}
money(){...}
}
class Son extends Father{ //子类
constructor(name,age,sno){
//这里需要注意,super必须在this之前,否则会报错
super(name,age);
this.sno = sno;
...
}
...
}
注意:在继承中采取"就近原则",如果一个属性或者方法在父类中找不到,那么就会去找父类的父类,如果还是找不到那就报错,否则返回该方法结果。
1.7 三个注意点
- 在ES6中类没有变量提升,所以必须 先定义类,然后才能实例化对象。
- 类里面的共有方法和属性要加this使用。
- **类里面this的指向问题。**constructor里面的this指向实例对象,方法里面的this指向这个方法的调用者。
二、构造函数和原型
2.1 概述
在典型的面向对象的语言中,都存在类的概念,但是在ES6之前,JS中并没有引入类的概念。
在ES6之前,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和他们的特征。
在ES6之前,创建对象的三种方式:
-
对象字面量
let obj1 = {}
-
new Object()
let obj2 = new Object();
-
自定义构造函数
function Person(name,age){ this.name = name; this.age = age; this.sing = function(){ console.log('我会唱歌!'); } } let obj3 = new Person('刘德华',18);
2.2 构造函数
构造函数是一种特殊的函数,主要用于对象的初始化,总是和new一起使用。我们可以将一些公共的属性和方法抽取出去出来,然后封装到这个函数中去。
在JS中,构造函数要注意以下几点:
- 构造函数用于创建某一类的对象,首字母要大写;
- 构造函数要与new一起使用来由意义。
2.3 new的执行过程(重要)
- 在内存中创建一个新的空对象;
- 让this指向这个新的对象;
- 执行构造函数里面的代码,给这个新对象添加属性和方法;
- 返回这个新对象(所以构造函数里面不需要return)
2.4 实例成员和静态成员
**实例成员:**实例成员就是构造函数内部通过this添加的成员,实例成员只能通过实例对象进行访问。
function Person(name,age){
this.name = name;
this.age = age;
this.sing = function(){
console.log('我会唱歌!');
}
//上面的this.name,this.age,this.sing都是实例成员
}
**静态成员:**在构造函数自身上添加的成员,当然也只能通过构造函数来访问。例如:
Person.sex = '男';
//Person是一个构造函数,sex是添加到它上面的一个方法
//因此每一个Person的实例对象创建后都会有一个属性sex='男'
2.5 构造函数原型对象prototype(重要)
构造函数方法很好用,但是存在浪费内存的问题。那么具体为什么会浪费内存呢?
首先,构造函数会占用内存中的一块区域,每创建一个实例对象就会开辟一个构造函数的内存空间,这样的话,有多少实例对象就会额外开辟多少构造函数的内存空间,而这些开辟的空间实际上都是一样的,因此保留一份就可以,其他的都是冗余。**如果打印 ldh.sing === zxy.sing ,你会发现结果是false,就是因为他们两个实例对象所指向的构造函数不是同一块内存。**下面用一个图来更好地解释:
上面这个问题的解决办法就是prototype,所有的实例对象都共用一个原型对象。构造函数通过原型分配的函数是所有对象所共享的。
JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。我们可以把那些不变的方法,直接定义在原型对象上面,这样所有对象的实例就可以共享这些方法。
在原型对象身上添加公共方法:
Star.prototype.sing = function(){
console.log('我会唱歌!');
}
关于原型的两个问题:
-
原型是什么?
一个对象,我们也称prototype为原型对象。
-
原型的作用是什么?
共享方法。
一般情况下,我们的公共属性定义到构造函数里面,但是公共方法我们放在原型对象身上。
2.6 对象原型 __ proto__ (重要)
对象都会有一个属性__ proto__ 指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为 __ proto__原型的存在。
如果实例对象的身上没有某一个方法,那么它就会去__ proto __ 所指的prototype上面去找。
### 2.7 __ proto__ 和prototype中的constructor属性
对象原型(__ proto__)和构造函数原型对象(prototype)里面都有一个constructor属性,constructor我们称为构造函数,因为它指回构造函数本身。
上面红色框中的输出结果为:两个指向的constructor实际上是一样的
constructor主要用于记录该对象引用哪个构造函数,它可以让原型对象重新指向原来的构造函数。
function Star(uname,age){
this.uname = uname;
this.age = age;
}
//很多情况下,我们需要手动利用constructor这个属性指回原来的构造函数
Star.prototype = {
sing:function(){
console.log('我会唱歌');
},
movie:function(){
console.log('我会演电影');
},
...
}
如果按照上面将公共方法都写在原型对象中,这样就会产生下面的问题:constructor都指向了Object
出现上面问题的原因:自己定义的prototype对象覆盖掉了系统中默认自带的prototype(系统生成的prototype中会自带constructor属性),因此在输出的时候会一直找到Object中的constructor。
解决:重新让constructor指回原来的构造函数
Star.prototype = {
constructor: Star, //指回原构造函数
sing:function(){
console.log('我会唱歌');
},
movie:function(){
console.log('我会演电影');
},
...
}
2.8 构造函数、实例、原型对象三者之间的关系
2.9 原型链
原型链的图如下:
**具体的过程:**首先,Star是一个构造函数,它会有一个prototype属性指向自己的原型对象,同时原型对象中会有constructor属性指向自己的构造函数Star。图中的ldh是Star的一个实例对象,它的__ proto__ 属性会指向Star的原型对象,(其实ldh.constructor也可以指向Star构造函数,只是因为Star原型对象中有constructor已经指向了Star,所以就没必要了),Star.prototype也是一个对象,也有一个属性__ proto__ 指向它的上一级Object的原型对象Object.prototype,紧接着Object.prototype.__ proto__ 又指向它的上一级null(因为Object是继承的顶端,所以它的上一级原型对象为null)。
实例对象.__ proto__ 指向该类(构造函数)的原型对象,该类(构造函数)原型对象的__ proto__ 又指向上一级的原型对象,然后一级一级这样向上指向,这样的一条链状的指向结构就是原型链。
2.10 JavaScript的成员查找机制
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有这个属性。
- 如果没有就查找他的原型(也就是__ proto__ 指向的prototype原型对象)。
- 如果还没有就查找原型对象的原型(Object的原型对象)。
- 以此类推一直找到Object为止(null)。
- __ proto__ 对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条线路。
2.11 原型对象this的指向
- 在构造函数中,里面的this指向的是实例对象
- 原型对象函数里面的this指向的也是实例对象
- 只有在调用的时候才能确定this指向的是谁。
2.12 扩展内置对象(例如Array等)
我们可以通过原型对象对原来的内置对象进行扩展自定义方法。比如给数组加自定义求偶数和的功能。
//以下代码就是给JS内置对象Array添加求和方法的代码
Array.prototype.sum = function(){
let sum = 0;
for(let i = 0; i < this.length; i++){
sum += this[i];
}
return sum;
}
//第一种调用方式
let arr= [1,2,3];
console.log(arr.sum()) //结果为6
//既然已经对Array的原型进行了扩展,那么也可以用它的构造函数new对象
let arr2 = new Array(11,22,33);
console.log(arr2.sum()); //结果为66
对于JavaScript中内置的对象,不能采用Array.prototype = {}这种方式来覆盖掉原来的原型,会报错。下面的写法是错误的:
Array.prototype = {
sum: function(){
let sum = 0;
for(let i = 0; i < this.length; i++){
sum += this[i];
}
return sum;
}
}
三、继承
在ES6之前,JavaScript中并没有继承的概念。都是通过构造函数+原型对象模拟实现继承,被称为组合继承。
3.1 call()
call()的作用:(1)调用某个函数;(2)可以修改函数运行时this的指向。
fun.call(thisArg,arg1,arg2,...)
thisArg:当前调用函数this的指向对象
arg1,arg2:传递的其他参数
function fn(){
console.log('')
}
}
## 三、继承
在ES6之前,JavaScript中并没有继承的概念。都是<font color='red'>通过**构造函数+原型对象**模拟实现继承,被称为**组合继承**。</font>
### 3.1 call()
call()的作用:(1)调用某个函数;(2)可以修改函数运行时this的指向。
```javascript
fun.call(thisArg,arg1,arg2,...)
thisArg:当前调用函数this的指向对象
arg1,arg2:传递的其他参数
function fn(){
console.log('')
}