目录
1.什么是构造函数
定义:在js中,使用new
关键字来调用的函数,被称为构造函数。
构造函数:本质也是函数;
构造函数的作用:创建对象。
2.为什么要使用构造函数
假如需要创建多个类似的对象,我们会书写很多重复的无意义代码。此时我们使用构造函数,方便快捷的创建一个对象。
如何封装一个构造函数
构造函数内部: 添加的时私有的属性和方法
构造函数的原型对象上添加共有的属性和方法
function Ball(type, size) {
//console.log(this);//Ball {}
// 私有属性
this.type = type;
this.size = size;
// 添加私有方法
this.play1 = function() {
console.log('私有方法');
}
}
// 原型对象上 :原型对象不需要创建,创建一个构造函数,该构造函数自然就拥有 prototype 属性,
// 该属性指向一个对象的引用,称之为原型对象
Ball.prototype.play2 = function() {
console.log('公有方法');
}
Ball.prototype.color = '公有属性'
// 通过new关键字创建出来的是实例化对象
var ball = new Ball('足球', 18);
//调用输出
console.log(ball);
ball.play1() //私有方法
ball.play2() //公有方法
console.log(typeof ball); //object
console.log(typeof Ball); //function
构造函数:
拥有 prototype 属性 : prototype 属性指向一个对象的引用,称之为原型对象 prototype
实例化对象:
拥有 __proto__ 属性: __proto__ 属性指向该实例化对象的原型对象
原型对象:
原型对象创建之初自然拥有两个属性: construcrtor 属性和 __proto__ 属性
construcrtor 属性指向原型对象的父构造函数 :指向可以随时更改
__proto__ 属性指向原型对象
3.构造函数的执行过程
function Animal(color){
this.color = color;
}
当一个函数创建完成后,我们并不知道它是不是一个构造函数。
像上面案例中,即使函数名首字母大写,我们也不确定它是构造函数。
只有当一个函数前用new
关键字来调用时,我们才能说它是一个构造函数。
var dog = new Animal("black") 此时dog叫做实例化对象
构造函数的执行过程有以下4个步骤。
- 当用
new
关键字调用构造函数时,会开辟一个新的内存空间,这个内容空间就是新的对象实例。 - 将构造函数内部的
this
指向当前的内存空间(新的对象)。 - 执行函数内的代码。(对象的属性和方法的赋值。)
- 将内存空间的地址作为返回值返回。
本质用函数表示大致是这样(不要细扣)
function fun(a, b) {
var obj = {};
function test() {
this.a = a
this.b = b;
}
test.call(obj)
return obj;
}
let res = fun(100, 'hello')
console.log(res);//{a: 100, b: "hello"}
4.构造函数的返回值
构造函数是没有返回值的,实际中用不到
存在返回值(面试题),但不是通过return 语句返回的, 构造函数默认返回的时创建的新对象(this)
- 构造函数的返回值时基本数据类型。返回值会被忽略
function Person(name){
this.name = name;
return "蔡徐坤";
}
var p = new Person("范丞丞");
console.log(p.name);//范丞丞
- 构造函数的返回值时引用数据类型。返回值会被忽略,this指向会丢失
function Person(name) {
this.name = name;
return { name: "蔡徐坤" };
//这样写就是错的 return ["name", "蔡徐坤" ];
}
var p = new Person("范丞丞");
console.log(p.name);//蔡徐坤
5.与普通函数的区别
构造函数和普通函数的区别:
1. 构造函数的函数名大写
2. 构造函数可以继承
3. 构造函数没有返回值,
4. 构造函数必须通过new关键字调用
5. 构造函数内部通过this添加属性和方法
5.1调用方式的不同
普通函数使用函数名
调用
构造函数通过new
关键字来调用
5.2 返回值不同
普通函数的返回值是函数内return的结果
构造函数的返回值是函数内创建对象的地址。
5.3 作用的不同
构造函数时专门用来创建对象。
普通函数的作用可以自由定义。
原型对象prototype
传统写法:
<script>
function Dog(name,age){
this.name = name;
this.age = age;
this.bark = function () {
alert("汪汪汪!")
}
}
var dog1 = new Dog("来福",3)
var dog2 = new Dog("常威",2)
dog1.bark();
dog2.bark();
</script>
因为上述方法造成了资源的浪费我们学习一个新的概念prototype属性
我们创建的每一个函数都有一个prototype属性,这个属性指向一个对象。而这个对象所有的属性和方法都会被构造函数拥有。
<script>
function Dog(name, age) {
this.name = name;
this.age = age;
}
var dog1 = new Dog("来福", 3);
var dog2 = new Dog("常威", 2);//调用
Dog.prototype.bark = function () {//这是一个原型对象的方法
alert("汪汪汪!")
}
Dog.prototype.breed = "哈士奇";//这是对原型对象的重新赋值
alert(dog1.bark === dog2.bark);//true 可以看出这是同一个原型对象
alert(dog1.breed)//哈士奇
alert(dog2.breed)//哈士奇
</script>
对象的封装
一般情况下,公共属性定义到构造函数里面,公共的方法定义到原型对象身上。
混合模式(构造函数+原型 模式)
<script>
function Dog(name, age, breed, color) {
this.name = name;
this.age = age;
this.breed = breed;
this.color = color;
}
Dog.prototype.show = function () {
alert("我只是一只小" + this.breed + "啊!")//公共的方法
}
Dog.prototype.bark = function () {//公共的方法
alert("汪汪汪!")
}
var d3 = new Dog("昊",4,"泰迪","浅棕色");
d3.bark();//汪汪汪
console.log(d3.name);//昊
</script>
原型链
每一个对象都有一个属性__proto__(隐式原型)
,这个属性指向构造函数的prototype(显式原型)
,也就是构造函数的原型对象。我们之所以可以在对象中使用原型对象中的方法和属性就是因为对象中有__proto__
属性的存在。
原型链概念:
每个实例化对象都具有一个原型链指针__proto__,该指针指向创建它的函数对象的原型对象,
而上一层的原型对象的结构依然类似,有自己的原型链指针,指向创建它的函数对象的原型对象。
这样利用__proto__一直指向Object的原型对象上,
而Object的原型对象用Object.proto = null表示原型链的最顶端。
这样由一系列__proto__串起来的原型对象就构成了原型链。
显式原型prototype的作用:用来实现基于原型的继承与属性的共享。
隐式原型__proto__的作用:构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj这个对象中的x属性时,如果在obj中找不到,那么就会沿着__proto__依次查找。
原型链(这副图从构造函数位置开始看)
<script>
function Dog(name,age,breed,color){
this.name = name;
this.age = age;
this.breed = breed;
this.color = color;
}
Dog.prototype.show = function () {
alert("我只是一只小" + this.breed + "啊!")//我只是一只小秋田啊!
}
Dog.prototype.bark = function () {
alert("汪汪汪!")
}
var aa= new Dog("来福",3,"秋田","明黄色");//传参(因为这一步,Dog()才是一个构造函数)
console.dir(aa.__proto__ === Dog.prototype);//true 构造函数的原型对象(Dog.prototype) 的指向和
实例对象的__proto__方法指向的都是一个,所以为true
console.log(aa.__proto__);// {show: ƒ, bark: ƒ} 这是两个原型对象
//-------------------------------------------------------
console.log(aa.__proto__.__proto__);// Object原型对象
console.log(aa.__proto__.__proto__.__proto__);//null
</script>
面向对象的三大特性:
封装(封装构造函数),继承,多态
面向对象 是一种编程思想。
区别: 比如吃饭:
面向过程:
红烧肉
1.买菜
2.洗菜
3.切菜
4.炒菜
5.出锅
6.开吃
面向对象:
1.找一个对象
2.让她给你做饭吃
继承:从父类那里继承父类的属性和方法。
多态:子类自己重新定义从父类继承过来的方法。或者新增一个父类没有的方法。
继承
ES6之前没有给我们提供 extends 继承。我们可以通过构造函数+原型对象的模式去模拟实现继承。这种方法也被称为组合继承。
<script>
//目地: 哈士奇继承狗的属性和方法
function Dog(age) {
this.age = age;
}
Dog.prototype.bark = function () {//Dog的原型对象
alert("汪汪汪!")
}
function Huskie(name, age) {
this.name = name;
this.age = age;
}
Huskie.prototype.bark = function () {//Huskie的原型对象
alert("汪汪汪!")
}
var hsq = new Huskie("二哈",2);//因为 new Huskie变成了一个构造函数
console.log(hsq.age); //实例对象.属性 输出 2
hsq.bark();//调用了Huskie的原型对象方法
</script>
使用call()方法实现继承
<script>
//目的:哈士奇用Dog的方法(这是个错误示范,因为this指向window)
function Dog(age){
console.log(this)//Window
// this.age = age;
}
Dog.prototype.bark = function(){
alert("汪汪汪!")
}
Dog.prototype.color = "黑白";
function Huskie(name,age){
Dog();//这里调用了Dog的方法(我们需要考虑到调用后this的指向,
看到上面this指向window,不是Dog这不是我们想要看到的,就无法正确调用Dog的方法)
this.name = name;
}
Huskie.prototype.bark = function(){
alert("汪汪汪!")
}
var hsq = new Huskie(2);
</script>
<script>
function Dog(age) {
console.log(this);//Huskie
this.age = age;
}
Dog.prototype.bark = function () {
alert("汪汪汪!")
}
Dog.prototype.color = "黑白";
function Huskie(name, age) {
Dog.call(this, age);
this.name = name;
}
Huskie.prototype.bark = function () {
alert("汪汪汪!")
}
var hsq = new Huskie( "二哈", 2);
console.log(hsq.age);//2
</script>
特点:
- 该方式是靠调用需要继承的构造函数来实现的,调用过程中使用call方法来改变this的指向。
- call是不可以继承父对象原型中的属性和方法。
- call是只能继承构造函数中的属性和方法。
优点:继承了父类构造函数中所有的属性和方法
缺点:不能继承父类原型对象中的属性和方法
看下面这张图你会好理解
使用prototype实现继承
在原型对象中有一个constructor属性,该属性指向该原型对象的构造函数。
<script>
//目的 哈士奇用dog的方法
function Dog(age){
this.age = age;
}
Dog.prototype.bark = function(){
alert("汪汪汪!")
}
function Huskie(name,age){//Huskie的构造函数
this.name = name;
}
// 这样写相当于让子类与父类指向了同一个原型对象。如果修改了子类的原型对象,则父类的原型对象也会随之修改
// Huskie.prototype = Dog.prototype;//(把Dog的原型对象方法给 哈士奇原型对象方法不就可以了吗,但是是不对的) 和下一比较
Huskie.prototype = new Dog(3);// 哈士奇的原型对象 = dog实例对象
Huskie.prototype.constructor = Huskie;//(哈士奇的构造函数)的原型对象的构造函数 = 哈士奇构造函数
var h = new Huskie();
console.log(h.age);//3
// 优点:继承了父级原型上的属性和方法**
// 缺点:实现化多个子类时,必须使用共同的属性值
// console.dir(h.constructor === h.__proto__.constructor);// 都指向了同一个构造函数 所以为 true
</script>
优点:继承了父级原型上的属性和方法
缺点:实现化多个子类时,必须使用共同的属性值。
组合式继承
<script>
function Dog(age){
this.age = age;
}
Dog.prototype.bark = function(){
alert("汪汪汪!")
}
function Huskie(name,age){
Dog.call(this,age);
this.name = name;
}
Huskie.prototype = new Dog();//哈士奇构造函数 . 原型对象 = Dog构造函数
Huskie.prototype.constructor = Huskie;//哈士奇构造函数 . 原型对象 . 构造函数 = 哈士奇构造函数
var h = new Huskie("二哈",5);
console.log(h.age);
h.bark();
</script>
<!-- call()方法优点:继承了父类构造函数中所有的属性和方法
prototype继承优点:继承了父级原型上的属性和方法 -->
多态
多态:子类自己重新定义从父类继承过来的方法。或者新增一个父类没有的方法。
<script>
//哈士奇继承了Dog的属性和方法后还有自己的属性和方法
function Dog(age) {
this.age = age;
}
Dog.prototype.bark = function() {
alert("汪汪汪!")
}
function Huskie(name, age) {
Dog.call(this, age);
this.name = name;
}
Huskie.prototype = new Dog();
Huskie.prototype.constructor = Huskie;
// Huskie.prototype.bark = function() {
// alert("嗷呜!")//这是哈士奇的方法
// }
var h = new Huskie("二哈", 5);
h.bark();
//当哈士奇没有自己方法时,就会调用Dog的方法汪汪汪
</script>
ES6面向对象
es5 类: 构造函数
es6 定义类: class
<script>
class Person {
// 可以直接在类中定义属性(弊端这样值就写死了,没法后续更改,看下面)
name = '孙悟空';
age = 18;
}
const per = new Person();
const per2 = new Person();
console.log(per);//Person {name: '孙悟空', age: 18}
console.log(per2);//Person {name: '孙悟空', age: 18}
console.log(per === per2);//false
</script>
class 类
// function Dog(breed,color,age){
// this.breed = breed;
// this.color = color;
// this.age = age;
// }
// Dog.prototype.bark = function(){
// alert("汪汪汪")
// }
// var dog = new Dog("泰迪","棕色",5);
// ------------- ES6写法 -------------------------
class Dog {
constructor(breed, color, age) {
this.breed = breed;
this.color = color;
this.age = age;
}
bark(){
alert("汪汪汪")
}
}
var dog = new Dog("泰迪","棕色",5);
console.log(dog.age, dog.breed, dog.color);// 实例对象.属性 5 "泰迪" "棕色"
dog.bark();//调用Dog方法 输出汪汪汪
- 在类的
实例
上调用方法实际上就是调用原型
上的方法
console.log(typeof Dog); //function
console.log(Dog === Dog.prototype.constructor); //true 都指向Dog构造函数
通过以上的代码,我们发现,class的本质是函数,类本身指向的就是构造函数。
ES6的class
可以看作是构造函数的语法糖。它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型写法更加清晰,更加符合面向对象编程的语法而已。
类必须使用new调用,否则会报错.普通构造函数不用new也可以执行(虽然没有太大意义),但类不行
//什么是语法糖
var arr = new Array();
var arr2 = [];//就是上面new Array的语法糖。
constructor方法
constructor方法是类的默认方法,叫做构造器,定义私有的属性
,通过new命令生成实例对象时,自动调用该方法.
一个类必须有constructor方法,如果没有显式定义,会默认添加一个空constructor方法
constructor默认返回实例对象(this),完全可以指定返回另外一个对象
//改变返回的对象为空对象,因此通过new Foo()创建的实例不是继承自Foo
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo // false
<script>
class Myperson {
// 定义私有的属性 通过 constructor 属性实现
constructor(name, age) {
this.name = name;
this.age = age;
}
// 类内部 定义的就是公有的方法; 定义多个公有的方法,中间不需要逗号
// 公有的方法添加到了原型对象上
eat() {
console.log('吃水果');
}
run() {
console.log('运动');
}
}
// 实例化对象
let p1 = new Myperson('张三', 18)
console.log(p1.name); //张三
p1.eat() //吃水果
</script>
ES6继承
<script>
class Dog {
constructor(breed, color, age) {
this.breed = breed;
this.color = color;
this.age = age;
}
bark() {
console.log("汪汪汪")
}
}
class Haskie extends Dog {
constructor(breed, color, age, name) {
//super在这里就相当于调用了父类的构造函数。
//super不可以省略,即使是个空括号也要写
super(breed, color, age);
this.name = name;
}
}
class Mao extends Dog {
//子类继承父类后,将获得父类中所有的属性和方法,
//也可以创建同名的属性或方法来对父类进行重写
bark() {
console.log("喵喵喵")
}
}
const h2 = new Haskie("哈士奇", "黑白", 5, "二哈");
const w2 = new Mao("珂珂", "白色", 2);
console.log(h2.breed, h2.color, h2.age, h2.name);// 哈士奇 黑白 5 二哈
h2.bark();//汪汪汪
console.log(w2.breed, w2.color, w2.age);//珂珂 白色 2
w2.bark();//喵喵喵
</script>
extends 这是继承 例子下面b继承a
上面用的super是什么呢,下面来解释
super
关键字,既可以当函数来使用,也可以当对象来使用。是一个方法
第一种情况:super作为函数调用时,表示父类的构造函数。
作为函数时,super()只能用在子类的构造函数中
class A { }
class B extends A {//B使用A的方法
constructor() {
super(); //不会报错
}
fn() {
}
}
class A { }
class B extends A {//B使用A的方法
constructor() {//构造函数B的属性
}
fn() {
super(); //报错(作为函数时,super()只能用在子类的构造函数中)
}
}
第二种情况
super作为对象时,在普通方法中,指向父类的原型对象。
class A {
p() {
return 1;
}
}
class B extends A {
constructor() {
super();
console.log(super.p());//1
}
}
let b = new B();
console.log(b.p());//1
静态属性static
什么是静态(static)方法?
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Demo{
static classMethod() {
return 'hello';
}
}
var res = Demo.classMethod();
console.log(res); // 'hello'
var demo = new Demo();
var res1 = demo.classMethod();
console.log(res1); // demo.classMethod is not a function
根据上面测试代码的结果,我们可以得到如下结论:
Demo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Demo类上调用(Demo.classMethod()),而不是在Demo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
静态方法this不是实例对象而是当前的类对象
<script>
/* 直接通过类调用的属性和方法被称为静态属性和静态方法 */
class MyClass {
// 使用static开头的属性是静态属性,方法是静态方法
static name = '哈哈';
static fn = () => {
// 注意:静态方法this不是实例对象而是当前的类对象
console.log("this指", this);
};
}
console.log(MyClass.name);
MyClass.fn();
</script>
注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。静态方法可以与非静态方法重名。
验证:静态方法可以与非静态方法重名
<script>
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
var foo = new Foo()
Foo.bar() // hello
Foo.baz() // hello
foo.baz() // world
</script>
上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
class类中的this指向
<script>
类中的所有代码都会在严格模式下执行
严格模式下其中一个特点就是,函数的this不在是window,而是undefined
class MyClass {
fn() {
console.log('-->', this);
}
}
const mc = new MyClass();
const test = mc.fn;//把方法赋值给一个变量,下面再去调用执行
mc.fn(); // mc
test(); // undefined
</script>
注意:
* 在类中方法的this不是固定的
* 以方法形式调用时,this就是当前的实例
* 以函数形式调用,this是undefined
* 在开发时,在有些场景下,我们希望方法中的this是固定的,不会因调用方式不同而改变
* 如果遇到上述需求,可以使用箭头函数来定义类中的方法
* 如果类中的方法是以箭头函数定义的,则方法中的this恒为当前实例,不会改变
class MyClass {
constructor() {
// this.fn = this.fn.bind(this);
//将fn方法的this绑定为当前实例(这是一种方法有点麻烦,所以我们用箭头函数)
}
fn = () => {
console.log('-->', this);
};
}
const mc = new MyClass();
const test = mc.fn;
mc.fn(); // MyClass {fn: ƒ}
test(); // MyClass {fn: ƒ}
const fn2 = function () {
console.log(this);//Window
};
fn2();