JS面向对象

227 阅读20分钟

面向对象

对象简介

对象( object)是“键值对”的集合,表示属性和值的映射关系

let obj = { 
    //属性名(键名,key)  属性值(value)  k : v对
    name: "云牧",
    age: 18,
    sex: "男",
    hobbies: ["打篮球","编程"]
}
//JS中,大括号表示对象

k和v之间用冒号分隔,每组k : v之间用逗号分隔,最后一个k :v对后可以不书写逗号

属性是否加引号

如果对象的属性键名不符合JS标识符命名规范,则这个键名必须用引号包裹

let obj = {
    "na-me": "云牧",
    age: 18,
    sex: "男",
    hobbies: ["打篮球","编程"]
}

属性的访问

可以用“点语法”访问对象中指定键的值

let obj = {
    name: "云牧",
    age: 18,
    sex: "男",
    hobbies: ["打篮球","编程"]
}

console.log(obj.name); //云牧
console.log(obj.age); //18
console.log(obj.hobbies); //["打篮球", "编程"]

如果属性名不符合JS标识符命名规范,则必须用方括号的写法来访问

let obj = {
    "na-me": "云牧",
};

console.log(obj["na-me"]);

如果属性名以变量形式存储,则必须使用方括号形式

let obj = {
    a: 1,
    b: 2,
    c: 3
};
let key = 'b';
console.log(obj.key); //undefined
console.log(obj[key]); //2

属性的更改

直接使用赋值运算符重新对某属性赋值即可更改属性

let obj = {
    a: 10
};
obj.a = 30;
obj.a++;

属性的创建

如果对象本身没有某个属性值,则用点语法赋值时,这个属性会被创建出来

let obj = {
    a: 10
};
obj.b = 40;

属性的删除

如果要删除某个对象的属性,需要使用delete操作符

let obj = {
    a: 1,
    b: 2
}
delete obj.a;

对象的方法

如果某个属性值是函数,则他被称为对象的方法

let obj1 = { 
    name: "云牧",
    age: 18,
    sex: "男",
    sayHello: function(){
        console.log("大家好 我是练习时长一年半的练习生坤坤")
    }
}

let obj1 = { 
    name: "夕颜",
    age: 18,
    sex: "男",
    sayHello: function(){
        console.log("大家好 我是练习时长一年半的练习生坤坤")
    }
}
//sayHello方法

//使用“点语法”可以调用对象的方法

xiaoming.sayHello() ;

方法也是函数,只不过方法是对象的“函数属性”,它需要用对象打点调用

对象的遍历

和遍历数组类似,对象也可以被遍历,遍历对象需要使用for. . .in...循环

使用for. . .in...循环可以遍历对象的每个键

for (var k in obj) {   //循环变量k它会一次成为对象的每一个键
    console.log("属性"+ k + "的值是"+ obj[k]);  //obj代表要遍历的对象
}

对象的深浅克隆

wvJUne.png

对象是引用类型值,这意味着:

不能用 let obj2 = obj1这样的语法克隆一个对象

使用==或者===进行对象的比较时,比较的是它们是否为内存中的同一个对象,而不是比较值是否相同

// 例子1
let obj1 = {
    a: 1,
    b: 2,
    c: 3
};
let obj2 = {
    a: 1,
    b: 2,
    c: 3
};
console.log(obj1 == obj2);      // false
console.log(obj1 === obj2);     // false

console.log({} == {});          // false
console.log({} === {});         // false

// 例子2
let obj3 = {
    a: 10
};
var obj4 = obj3;
obj3.a ++;
console.log(obj4);      // {a: 11}

浅克隆

只克隆对象的“表层”,如果对象的某些属性值又是引用类型值,则不进一步克隆它们,只是传递它们的引用

let obj1 = {
    a: 1,
    b: 2,
    c: [44, 55, 66]
};
// 实现浅克隆
let obj2 = {};
for (let k in obj1) {
    // 每遍历一个k属性,就给obj2也添加一个同名的k属性
    // 值和obj1的k属性值相同
    obj2[k] = obj1[k];
}
// 为什么叫浅克隆呢?比如c属性的值是引用类型值,那么本质上obj1和obj2的c属性是内存中的同一个数组,并没有被克隆分开。
obj1.c.push(77);
console.log(obj2);                 // obj2的c属性这个数组也会被增加77数组
console.log(obj1.c == obj2.c);      // true,true就证明了数组是同一个对象

深克隆

克隆对象的全貌,不论对象的属性值是否又是引用类型值,都能将它们实现克隆

<script>
    var obj1 = {
        a: 1,
        b: 2,
        c: [33, 44, {
            m: 55,
            n: 66,
            p: [77, 88]
        }]
    };

    // 深克隆
    function deepClone(o) {
        let result;

        if (Array.isArray(o)) {
            // 要判断o是对象还是数组

            //数组
            result = [];
            for (let i = 0; i < o.length; i++) {
                result.push(deepClone(o[i]));
            }
        } else if (typeof o == "object") {
            // 对象
            result = {};

            for (let k in o) {
               	
                result[k] = deepClone(o[k]);
            }
        } else {
            // 基本类型值
            result = o;
        }
        return result;
    }


    let obj2 = deepClone(obj1);
    console.log(obj2);

    console.log(obj1.c == obj2.c);      // false

    obj1.c.push(99);
    console.log(obj2);                  // obj2不变的,因为没有“藕断丝连”的现象

    obj1.c[2].p.push(999);
    console.log(obj2);                  // obj2不变的,因为没有“藕断丝连”的现象

</script>

上下文对象this

我们先来看一句话

这是非常好的习惯,值得表扬 (这字我们一脸懵逼 不知道指代什么,需要有前言后语)

比如

垃圾分类,这是非常好的习惯,值得表扬

随手关灯,这是非常好的习惯,值得表扬

遛狗栓绳,这是非常好的习惯,值得表扬

课后复习,这是非常好的习惯,值得表扬

早睡早起,这是非常好的习惯,值得表扬

函数的上下文

函数中可以使用this关键字,它表示函数的上下文

与中文中“这”类似,函数中的this具体指代什么必须通过调用函数时的“前言后语”来判断

举例

let yunmu = {
    nickname: "云牧",
    age: 12,
    sayHello: function() {	
        console.log("我是" + this.nickname + ",我" + this.age + "岁了");
    }
}

//对象打点调用这个函数
yunmu.sayHello(); //此时的this指的是yunmu对象本身  


//再看这个
let say = yunmu.sayHello; //将函数提出来 单独存储为变量
say(); //直接圆括号调用这个函数而不是对象打点调用了

调用方式不同也导致输出结果不一样

0IkSwd.png

得出一个结论

函数的上下文由调用方式决定

同一个函数,用不同的形式调用它,则函数的上下文 不同

情形1:对象打点调用函数,函数中的this指代这个打点的对象

yunmu.sayHello();

情形2:圆括号直接调用函数,函数中的this指代window对象

let say = yunmu.sayHello; 
say();

题目

let obj = {
    a: 1,
    b: 2,
    fn: function(){
        console.log(this.a + this.b);
    }
}








//这个函数的this指代什么?

//不知道  因为它的上下文只有当被调用上下文才确定
let obj = {
    a: 1,
    b: 2,
    fn: function(){
        console.log(this.a + this.b);
        console.log(this === obj);
    }
}

obj.fn(); //3 true

var a = 10;
var b = 2;
let newFn = obj.fn;
newFn(); // 12 false

函数的上下文(this关键字)由调用函数的方式决定,function是“运行时上下文”策略

函数如果不调用,则不能确定函数的上下文

规则1

规则1:对象打点调用它的方法函数,则函数的上下文是这个打点的对象

对象.方法();

题目1

<script>
    function add(){
        console.log(this.a + this.b);
    }

    let obj = {
        a: 66,
        b: 33,
        fn:add
    };

    obj.fn(); //构成对象.方法()的形式,适用规则1
</script>

运行结果是

99

题目2

<script>
    let obj1 = {
        a: 1,
        b: 2,
        fn: function(){
            console.log(this.a + this.b);
        }
    }

    let obj2 = {
        a: 3,
        b: 4,
        fn: obj1.fn
    }
    obj2.fn() //构成对象.方法()的形式,适用规则1
</script>

运行结果

7

题目3

<script>
    function outer(){
        var a = 11;
        var b = 22;
        return {
            a: 33,
            b: 44,
            fn: function(){
                console.log(this.a + this.b);
            }
        }
    }

    outer().fn(); //构成对象.方法()的形式,适用规则1
</script>

运行结果

77

题目4

<script>
    function fn(){
        console.log(this.a + this.b);
    }

    let obj = {
        a: 1,
        b: 2,
        c: [{
            a: 3,
            b: 4,
            c : fn
        }]
    }
    var a = 5;
    obj.c[0].c(); //构成对象.方法()的形式,适用规则1
</script>

运行结果

7

规则2

规则2:圆括号直接调用函数,则函数的上下文是window对象

函数();

题目1

<script>
    //圆括号直接调用  函数上下文就是window对象

    let obj = {
        a: 1,
        b: 2,
        fn: function(){
            console.log(this.a + this.b);
        }
    }

    var a = 3;
    var b = 4;

    let fn = obj.fn; //构成函数()的形式,适用规则2
    fn(); 
</script>

运行结果

7

题目2

<script>
    function fun() {
        return this.a + this.b;
    }

    var a = 1;
    var b = 2;

    let obj = {
        a: 3,
        b: fun(),  //构成函数()的形式,适用规则2
        fun: fun,
    };

    let result = obj.fun(); //构成对象.方法()的形式,适用规则1

    console.log(result);
</script>

运行结果

6

规则3

规则3︰数组(类数组对象)枚举出函数进行调用,上下文是这个数组(类数组对象)

数据[下标]();

题目1

<script>
    let arr = [
        "A",
        "B",
        "C",
        function () {
            console.log(this[0]); 
        },
    ];

    arr[3]();  //适用规则3
</script>

运行结果

A

题目2

<script>
    function fn() {
        arguments[3]();  //适用规则3
    }
    fn("A", "B", "C", function () {
        console.log(this[1]);
    });
</script>

运行结果

B

规则4

规则4:IIFE中的函数,上下文是window对象

(function(){})();

题目1

<script>
    var a = 1;
    var obj = {
        a: 2,
        fun: (function () {
            var a = this.a;
            return function () {
                console.lcssog(a + this.a);
            };
        })() //适用规则4
    };

    obj.fun();//适用规则1
</script>

运行结果

3

规则5

规则5:定时器、延时器调用函数,上下文是window对象

setInterval(函数,时间);
setTimeout(函数,时间);

题目1

<script>
    let obj = {
        a: 1,
        b: 2,
        fun: function(){
            console.log(this.a + this.b);
        }
    }

    var a = 3;
    var b = 4;

    setTimeout(obj.fun , 2000) //适用规则5


    //注意这个不是由延时器或者定时器调用的
    setTimeout(function(){
        obj.fun(); //适用规则1
    },2000)

</script>

运行结果

7
3

规则6

规则6:事件处理函数的上下文是绑定事件的DOM元素

DOM元素.onclick = function () {}

小案例1

请实现效果:点击哪个盒子,哪个盒子就变绿,要求使用同一个事件处理函数实现

<style>
    div {
        float: left;
        width: 200px;
        height: 200px;
        border: 2px solid black;
        margin: 0 10px;
    }
</style>
</head>
<body>
    <div id="box1"></div>
    <div id="box2"></div>
    <div id="box3"></div>
    <script>


        let box1 = document.getElementById("box1");
        let box2 = document.getElementById("box2");
        let box3 = document.getElementById("box3");

        box1.onclick = changeColor;
        box2.onclick = changeColor;
        box3.onclick = changeColor;


        function changeColor() {
            this.style.backgroundColor = "green";
        }
    </script>
</body>

小案例2

请实现效果:点击哪个盒子,哪个盒子在2000毫秒后就变绿,要求使用同一个事件处理函数实现

<style>
    div {
        float: left;
        width: 200px;
        height: 200px;
        border: 2px solid black;
        margin: 0 10px;
    }
</style>
</head>
<body>
    <div id="box1"></div>
    <div id="box2"></div>
    <div id="box3"></div>
    <script>


        let box1 = document.getElementById("box1");
        let box2 = document.getElementById("box2");
        let box3 = document.getElementById("box3");

        box1.onclick = changeColor;
        box2.onclick = changeColor;
        box3.onclick = changeColor;


        function changeColor() {
            // 备份上下文  有人喜欢that  _this
            let self = this;
            setTimeout(function () {
                self.style.backgroundColor = "green";
            }, 2000);
        }
    </script>

call和apply和bind

call和apply能间接的调用函数并且指定函数的上下文

函数.call(上下文);
函数.apply(上下文);
<script>
    function sum(){
        alert(this.chinese + this.math + this.english)
    }

    let xiaoming = {
        chinese: 80,
        math: 90,
        english: 90,
        sum:sum
    };

    xiaoming.sum();
    sum();
    sum.call(xiaoming);
    sum.apply(xiaoming);
</script>

call和apply的区别

<script>
    function sum(b1, b2) {
        alert(this.chinese + this.math + this.english + b1 + b2);
    }

    let xiaoming = {
        chinese: 80,
        math: 90,
        english: 90,
        sum: sum,
    };

    sum.call(xiaoming, 5, 3); //call要用逗号罗列参数  错位传参
    sum.apply(xiaoming, [5, 3]); //apply要把参数写到数组中
    //所有实参放在一个数组  数组第一个数对应第一个形参
</script>

到底使用call还是apply?

<script>
    function fn1() {
        fn2.apply(this, arguments); //this值是fn1的上下文window
    }

    function fn2(a, b) {
        console.log(a + b);
    }

    fn1(11, 22);
</script>

0Idmk9.png

事件函数可以改变this指向吗?

let xiyan = {name:"夕颜" , age:14}

function fn(){
    console.log("fn执行了");
    console.log(this);
}
// fn.call()函数会自执行 返回undefined 
document.onclick  = fn.call(xiyan);

现在我们既要改变函数this指向

不需要立即执行函数

let xiyan = {name:"夕颜" , age:14}

function fn(){
    console.log("fn执行了");
    console.log(this);
}

document.onclick  = fn.bind(xiyan);

//内部实现原理类似于
document.onclick  = function(){
    fn.call(xiyan)
}

构造函数与类

let tom = {
    name: "汤姆",
    age: 18,
    company: "潭州教育",
    sayName: function () {
        console.log(`我叫${this.name},今年${this.age},来自${this.company}`);
    },
};

let jerry = {
    name: "杰瑞",
    age: 20,
    company: "潭州教育",
    sayName: function () {
        console.log(`我叫${this.name},今年${this.age},来自${this.company}`);
    },
};

let zeek = {
    name: "扎克",
    age: 30,
    company: "潭州教育",
    sayName: function () {
        console.log(`我叫${this.name},今年${this.age},来自${this.company}`);
    },
};

tom.sayName();
jerry.sayName();
zeek.sayName();

此时一个一个定义有点麻烦,我们可以封装一下 :

//工厂模式
function createTeacher(name, age) {
    //原料
    let obj = {};

    //加工
    obj.name = name;
    obj.age = age;
    obj.company = "潭州教育";
    obj.sayName = function () {
        console.log(`我叫${this.name},今年${this.age},来自${this.company}`);
    };
    //产出
    return obj;
}

let tom = createTeacher("tom", 18);
let jerry = createTeacher("jerry", 18);
let zeek = createTeacher("zeek", 18);

tom.sayName();

现在,我们学习一种新的函数调用方式:

new.函数()

你可能知道new操作符和“面向对象”息息相关,但是现在,我们先不探讨它的“面向对象”意义,而是先

把用new调用函数的执行步骤和它上下文弄清楚

JS规定,使用new操作符调用函数会进行“四步走”:

1)函数体内会自动创建出一个空白对象

2)函数的上下文 (this)会指向这个对象

3)函数体内的语句会执行

  1. 函数会自动返回上下文对象,即使函数没有return语句

例子

<script>
    function fn(){
        this.a = 3;
        this.b = 5;
    }

    let obj = new fn();
    console.log(obj);
</script>

相当于

<script>
    function fn(){
       	// 创建{} ①
        // this => {} ②
        this.a = 3;
        this.b = 5;//执行语句 ③
        //return this ④
    }

    let obj = new fn(); //返回的上下文对象被变量obj接收
    console.log(obj);
</script>

例子

<script>
    function fn(){
        this.a = 3;
        this.b = 5;
        let m = 60;
        if(this.a > this.b){
            this.c = m+10;
        }else{
            this.c = m+20;
        }
    }

    let obj = new fn();
    console.log(obj);
</script>

0IBnYj.png

构造函数

什么是构造函数

我们将之前书写的函数进行一下小改进:

<script>
    function People(name, age, sex) { //接收三个参数
        this.name = name;
        this.age = age; //this绑定同名属性
        this.sex = sex;
    }

    let yunmu = new People("云牧", 18,"男"); //传入三个参数
    let daiyu = new People("黛玉", 14,"女");
    let xiaoming = new People("小明", 20, "男");

    console.log(yunmu);
    console.log(daiyu);
    console.log(xiaoming);
</script>

运行结果如下图

0TnZvR.png

用new调用一个函数,这个函数就被称为**“构造函数”**,任何函数都可以是构造函数,只需要用new调用它

顾名思义,构造函数用来“构造新对象”,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化

构造函数必须用new关键字调用,否则不能正常工作,正因如此,开发者约定构造函数命名时首字母要大写

请判断对错:

一个函数名称首字母大写了,它就是构造函数

一定要记住:一个函数是不是构造函数,要看它是否用new调用,而至于名称首字母大写,完全是开发

者的习惯约定

如果不用new调用构造函数

  function People(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    
People("云牧", 18,"男");
People("黛玉", 16,"女");
// 此时这个函数单独执行没有太大意义(相当于给window加属性),只有在new执行的时候才能发挥其作用

尝试为对象添加方法

<script>
    function People(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.sayHello = function(){
            console.log(`我是${this.name},今年${this.age}啦`);
        }
        this.sleep = function(){
            console.log(this.name + "开始睡觉觉了zzzz");
        }
    }

    let yunmu = new People("云牧", 18,"男");
    let daiyu = new People("黛玉", 14, "女");
    let xiaoming = new People("小明", 20, "男");
    yunmu.sayHello();
    daiyu.sayHello();
    xiaoming.sayHello();
</script>

结果如下

0TnWZV.png

类和实例

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.sayHello = function () {
        console.log(`我是${this.name},今年${this.age}啦`);
    };
    this.sleep = function () {
        console.log(this.name + "开始睡觉觉了zzzz");
    };
}

let yunmu = new Person("云牧", 18,"男"); 
let daiyu = new People("黛玉", 14, "女");
let xiaoming = new People("小明", 20, "男");
//我们通过new调用Person函数创建的对象都具有name,age,sex,sayHello()
//我可以将构造函数Person认为是类。yunmu, xiyan , xioaoming是实例
//通过new构造函数得到对象的过程,称之为实例化

类好比是蓝图

如同“蓝图”一样,类只描述对象会拥有哪些属性和方法,但是并不具体指明属性的值

0I69MR.png

实例是具体的对象

0I6xTf.png

0IckXn.png

0IcV00.png

比如

“狗”是类,“史努比”是实例、“小白”是实例

Java、C++等是“面向对象”(object-oriented)语言

Javascript是“基于对象”(object-based)语言

Javascript中的构造函数可以类比于OO语言中的“类,写法的确类似,但和真正OO语言还是有本质不同,在后面还将看见和其他OO语言完全不同的、特有的原型特性

原型和原型链

任何函数都有prototype属性,prototype是英语“原型”的意思

prototype属性值是个对象,它默认拥有constructor属性指回函数

0IcX34.png

function fn(a , b){
    return a + b;
}

console.log(fn.prototype); 
console.log(typeof fn.prototype);
console.log(fn.prototype.constructor === fn); 

普通函数来说的prototype属性没有任何用处,而构造函数的prototype属性非常有用

构造函数的prototype属性是它的实例的原型

0IRSk6.png

function People(name,age,sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
//  实例化
let xiaoming = new People("小明", 18, "男");
// 测试三角关系是否存在
console.log(People.prototype === xiaoming.__proto__); //true

原型链查找

JavaScript规定:实例可以打点访问它的原型的属性和方法,这被称为“原型链查找”

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}

//往原型上添加nationality属性
People.prototype.nationality = "美国";//在构造函数的prototype上添加company属性

let xiaoming = new People("小明", 18, "男");

console.log(xiaoming.nationality); //实例可以打点访问原型的属性和方法

0IhjgO.png

遮蔽效应

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
People.prototype.nationality = "美国";

let xiaoming = new People("小明", 18, "男");

console.log(xiaoming.nationality); //美国


let yunmu = new People("云牧", 18, "男");
yunmu.nationality = "中国";
console.log(yunmu.nationality); //中国

hasOwnProperty

hasOwnProperty方法可以检查对象是否真正“自己拥有”某属性或者方法

<script>
    function People(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    People.prototype.nationality = "美国";

    let xiaoming = new People("小明", 18, "男");

    console.log(xiaoming.hasOwnProperty("name")); // true

    console.log(xiaoming.hasOwnProperty("age")); // true

    console.log(xiaoming.hasOwnProperty("sex")); // true

    console.log(xiaoming.hasOwnProperty("nationality")); //false
</script>

in

in运算符只能检查某个属性或方法是否可以被对象访问

不能检查是否是自己的属性或方法

<script>
    function People(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    People.prototype.nationality = "美国";

    let xiaoming = new People("小明", 18, "男");

    console.log("name" in xiaoming); // true

    console.log("age" in xiaoming); // true

    console.log("sex" in xiaoming); // true

    console.log("nationality" in xiaoming); //true
</script>

在prototype上添加方法

在前面,我们把方法都是直接添加到实例身上

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.sayHello = function () {
        console.log(`我是${this.name},今年${this.age}啦`);
    };
    this.sleep = function () {
        console.log(this.name + "开始睡觉觉了zzzz");
    };
}

let yunmu = new People("云牧", 18, "男");
let daiyu = new People("黛玉", 14, "女");
let xiaoming = new People("小明", 20, "男");
//这个new了三个对象 三个对象分别有name,age,sex属性 和 sayHello,sleep方法  
//所有每 new一个对象会创建一个 不同的函数

console.log(yunmu.sayHello == daiyu.sayHello); //false
console.log(yunmu.sleep === daiyu.sleep); //false

方法要写到prototype上

0IIOCq.md.png

<script>
    function People(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    People.prototype.sayHello = function () {
        console.log(`我是${this.name},今年${this.age}啦`);
    };

    People.prototype.sleep = function () {
        console.log(this.name + "开始睡觉觉了zzzz");
    };

    People.prototype.growup = function () {
        this.age++;
    };

    let yunmu = new People("云牧", 18, "男");
    let daiyu = new People("黛玉", 16, "女");

    yunmu.growup();
    yunmu.growup();
    yunmu.sayHello();

    daiyu.growup();
    daiyu.sayHello();
    yunmu.sleep();
    daiyu.sleep();

    console.log(yunmu.sayHello == daiyu.sayHello);
    console.log(yunmu.sleep === daiyu.sleep);
</script>

运行结果如下图

0TMpDI.png

原型链的终点

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}

let xiaoming = new People("小明", 18, "男");


console.log(People.prototype.__proto__ == Object.prototype); //true
console.log(People.__proto__.__proto__ == Object.prototype); //true
console.log(Object.prototype.__proto__); // null

console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); //true
console.log(Object.prototype.hasOwnProperty("toString")); //true

image-20201014233021503

关于数组的原型链

0IO7h4.png

<script>
    let arr = [12, 34, 56];

    console.log(arr.__proto__ === Array.prototype); //true
    console.log(arr.__proto__.__proto__ === Object.prototype); //true

    console.log(Array.prototype.hasOwnProperty("pop")); //true
    console.log(Array.prototype.hasOwnProperty("sort")); //true
</script>

内置构造函数和继承

先来看两幅图

两个无关的类

0TQfX9.png

People类和Student类的关系

image-20211204172329995](imgchr.com/i/0TlAXj)

People类拥有的属性和方法Student类都有,Student类还扩展了一些属性和方法

Student是一种People,两类之间是“**is a kind of”**关系

这就是继承关系: Student类继承自People类

继承

继承描述了两个类之间的“is a kind of”关系

比如学生“是一种“人“,所以人类和学生类之间就构成继承关系

People是**“父类”(或“超类”、“基类”) ; Student是“子类”**(或“派生类”)

子类丰富了父类,让类描述得更具体、更细化

0T1SKJ.png

更多的继承关系举例

0T12L9.png

1.通过原型链实现继承

image-20211204123418581

实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能定义自己特有的属性和方法

让子类构造函数的prototype,指向父类的一个实例

使用JavaScript特有的原型链特性来实现继承,是普遍的做法

0TUTr8.png

//父类 人类
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    // this.arr = [33, 44, 55];
}

People.prototype.sayHello = function () {
    console.log(`我是${this.name},今年${this.age}啦`);
};

People.prototype.sleep = function () {
    console.log(this.name + "开始睡觉觉了zzzz");
};

//子类 学生类
function Student(name, age, sex, scholl, studentNumber) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.scholl = scholl;
    this.studentNumber = studentNumber;
}
// 核心语句 实现继承
Student.prototype = new People();

Student.prototype.study = function(){
    console.log(this.name + "正在学习");
}
Student.prototype.exam = function(){
    console.log(this.name + "正在考试,奥利给!");
}

//子类可以重写,复写(override) 父类方法sayHello
Student.prototype.sayHello = function(){
    console.log( `敬礼! 您好,我是${this.name},${this.sex},我来自${this.scholl},我${this.age}了`);
}

// 实例化
let hanmeimei = new Student("韩梅梅" , 12, "女", "中心小学" , 12110);

hanmeimei.study();
hanmeimei.exam();
hanmeimei.sayHello();
hanmeimei.sleep();

let yunmu = new People("云牧", 18, "男");
yunmu.sayHello();

运行结果如下

下载

通过原型链实现继承的问题

  • 问题1:如果父类的属性中有引用类型值,则这个属性会被所有子类的实例共享;
  • 问题2:子类的构造函数中,往往需要重复定义很多超类定义过的属性。即子类的构造函数写的不够优雅;

2.借用构造函数

  • 为了解决原型中包含引用类型值所带来问题和子类构造函数不优雅的问题,开发人员通常使用一种叫做**“借助构造函数”的技术,也被称为“伪造对象”“经典继承”**
  • 借用构造函数的思想非常简单:在子类构造函数的内部调用超类的构造函数,但是要注意使用call()绑定上下文

我们将上文例子稍微改造

//父类 人类
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.arr = [33, 44, 55];
}

//子类 学生类
function Student(name, age, sex, scholl, studentNumber) {
    //子类中使用call执行父类构造函数 绑定this并传值
    People.call(this, name, age, sex);
    this.scholl = scholl;
    this.studentNumber = studentNumber;
}

const xiaoming = new Student("小明", "男", 12, "中心学校", 100666);
console.log(xiaoming);
xiaoming.arr.push(77); 
console.log(xiaoming.arr); // [33, 44, 55, 77]
console.log(xiaoming.hasOwnProperty("arr")); // true

const xiaohong = new Student("小红", "女", 11, "中心学校", 100667);
console.log(xiaohong.arr); // [33, 44, 55]
console.log(xiaohong.hasOwnProperty("arr")); // true

3.组合继承

就是将原型链继承和构造函数继承组合在一起,继承两个优点,也做伪经典继承

通过调用父类构造函数,继承父类的属性并保留传参的优点,

然后再通过将父类实例作为子类原型,实现函数复用

组合继承是]avaScript中最常用的继承模式

//子类 学生类
function Student(name, age, sex, scholl, studentNumber) {
    //子类中使用call执行父类构造函数 绑定this并传值
    People.call(this, name, age, sex);
    this.scholl = scholl;
    this.studentNumber = studentNumber;
}
// 核心语句 实现继承
Student.prototype = new People();

组合继承的缺点

  • 组合继承最大的问题就是无论什么情况下,都会调用两次超类的构造函数
  • 一次是在创建子类原型的时候,另一次是在子类构造函数的内部

4.原型式继承

IE9+开始支持0bject.create()方法,可以根据指定的对象为原型创建出新对象

const obj2 = Object.create(obj1);

下载 (1)

const obj1 = {
    a: 1,
    b: 2,
    c: 3,
    test() {
        console.log(this.a + this.b);
    }
};

const obj2 = Object.create(obj1, {
    // 为obj2添加新属性
    d: {
        value: 99
    }, 
    // 访问时会遮蔽原型上的a属性
    a: {
        value: 666
    }
});


console.log(obj2.__proto__ === obj1);       // true
console.log(obj2.a); // 666
console.log(obj2.b); // 2
console.log(obj2.c); // 3
console.log(obj2.d); // 99
obj2.test(); // 668

如果想要新对象与现有对象"类似", 使用Object.create(),不必大张旗鼓创建构造函数

Object.create()的兼容性写法

  • 如何在低版本浏览器中实现0bject.create()呢?
// 函数的功能是以obj为原型创建新对象
function object(obj) { 
    // 创建一个临时构造函数
    function F() {}
    // 让这个临时构造函数的prototype指向o,这样一来它new出来的对象,__proto__指向了o
    F.prototype = obj;
    // 返回F的实例
    return new F();
}

5.寄生式继承

  • 编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法

image-20211204144529995

const o1 = {
    name: "小明",
    age: 12,
    sex: "男",
};

const o2 = {
    name: "小红",
    age: 11,d
    sex: "女",
};

// 寄生式继承
function f(o) {
    // 以o为原型创建出新对象
    const p = Object.create(o);
    // 补充两个方法
    p.sayHello = function () {
        console.log("你好,我是" + this.name + "今年" + this.age + "岁了");
    };
    p.sleep = function () {
        console.log(this.name + "正在睡觉");
    };

    return p;
}

// 以o1对象为原型得到p1对象
const p1 = f(o1);
p1.sayHello(); // 你好,我是小明今年12岁了

// 以o2对象为原型得到p2对象
const p2 = f(o2);
p2.sayHello(); // 你好,我是小红今年11岁了
  • 寄生式继承就是编写一个函数,它可以“增强对象”

  • 只要把对象传入这个函数,这个函数将以此对象为“基础”创建出新对象,并为新对象赋予新的预置方法

  • 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式

寄生式继承的缺点

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,即“方法没有写到prototype上”
console.log(p1.sayHello == p2.sayHello); // false

6.寄生组合式继承

  • 寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
  • 其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,
  • **我们所需要的无非就是超类型原型的一个副本而已。
  • 本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function People(name) {
    this.name = name;
}

People.prototype.eat = function () {
    console.log(this.name + " is eating");
};

function Stundent(name, age) {
    People.call(this, name);
    this.age = age;
}
Stundent.prototype = Object.create(People.prototype);
// prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
Stundent.prototype.contructor = Stundent;

Stundent.prototype.study = function () {
    console.log(this.name + " is studying");
};

// 测试
let xiaoming = new Stundent("xiaoming", 16);
console.log(xiaoming.name); // xiaoming
xiaoming.eat(); // xiaoming is eating
xiaoming.study(); // xiaoming is studying

图示如下:

image-20211204154315064

7.ES6中class 的继承

  • ES6中引入了class关键字,class可以通过extends关键字实现继承
  • 还可以通过static关键字定义类的静态方法
  • 这比 ES5 的通过修改原型链实现继承,要清晰和方便很多
  • 需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的
class Person {
    //类的构造方法
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    //定义方法
    showName() {
        console.log("调用父类的方法");
        console.log(this.name, this.age);
    }
}
const p1 = new Person("kobe", 39);
console.log(p1);

//定义一个子类
class Student extends Person {
    constructor(name, age, salary) {
        super(name, age); //通过super调用父类的构造方法
        this.salary = salary;
    }
    showName() {
        //在子类自身定义方法
        console.log("调用子类的方法");
        console.log(this.name, this.age, this.salary);
    }
}
const s1 = new Student("yunmu", 18, 1000000);
console.log(s1);
s1.showName(); // yunmu 18 1000000

instance运算符

  • instanceof运算符用来检测“某对象是不是某个类的实例”,比如:

image-20211204154351358

function People() {}
function Student() {}
Student.prototype = Object.create(People.prototype);
const xiaoming = new Student();

// 测试instanceof
console.log(xiaoming instanceof Student); // true
console.log(xiaoming instanceof People); // true

还是此图,小明可以沿着原型链找到Student和People的prototype属性 所以instanceof结果为true

image-20211204154315064

内置构造函数

  • JavaScript有很多内置构造函数,比如Array就是数组类型的构造函数,Function就是函数类型的构造函数,object就是对象类型的构造函数
  • 内置构造函数非常有用,所有该类型的方法都是定义在它的内置构造函数的prototype上的,我们可以给这个对象添加新的方法,从而拓展某类型的功能

image-20211204160110373

// 数组的内置构造函数,任何的数组字面量都可以看做是Array的实例
console.log([1, 2] instanceof Array); // true
console.log([] instanceof Array); // true

// 创建了长度为5的空数组
const arr = new Array(5);
console.log(arr); //[ <5 empty items> ]
console.log(arr.length); // 5

// 函数的内置构造函数,任何的函数字面量都可以看做是Function的实例
function fun() {}
function add(a, b) {
    return a + b;
}
console.log(fun instanceof Function); // true
console.log(add instanceof Function); // true

const decrement = new Function("a", "b", "return a - b");
console.log(decrement(8, 3)); // 5

// 对象的内置构造函数
console.log({ a: 1 } instanceof Object); // true
console.log({} instanceof Object); // true
const o = new Object();
o.a = 2;
o.b = 6;
console.log(o); // { a: 2, b: 6 }

扩展方法

// push pop 等方法实际都是在数组构造函数原型上的方法
console.log(Array.prototype.hasOwnProperty("push")); // true
console.log(Array.prototype.hasOwnProperty("pop")); // true
console.log(Array.prototype.hasOwnProperty("splice")); // true

// 拓展qiuhe方法
Array.prototype.total = function () {
    // 累加器
    let sum = 0;
    // this表示调用total()方法的数组
    for (let i = 0; i < this.length; i++) {
        sum += this[i];
    }
    // 返回结果
    return sum;
};

const arr1 = [1, 2, 3, 4, 5];
const result1 = arr1.total();
console.log(result1); // 15

const arr2 = [2, 8];
const result2 = arr2.total();
console.log(result2); // 10

基本类型值的“包装类”

  • Number、String、Boolean是三个基本类型值的包装类,用new调用它们可以生成“对象”版本的基本类型值
const o = new Number(3);
console.log(o);
console.log(typeof o); // object
console.log(5 + o); // 8

const s = new String("abc");
console.log(s);
console.log(typeof s); // object
console.log(String.prototype.hasOwnProperty("slice")); // true
console.log(String.prototype.hasOwnProperty("substring")); // true
console.log(String.prototype.hasOwnProperty("substr")); // true

const b = new Boolean(true);
console.log(b);
console.log(typeof b); // object

内置构造函数的关系

  • 0bject.prototype是万物原型链的终点。JavaScript中函数、数组皆为对象,以数组为例,完整原型链是这样的:

image-20211204163050910

测试:

console.log([1, 2].__proto__ === Array.prototype); // true
console.log([1, 2].__proto__.__proto__ === Object.prototype); // true

console.log([] instanceof Object); // true
console.log([] instanceof Array); // true

console.log(Object.prototype.__proto__); // null null没有__proto__
  • 任何函数都可以看做是 Function “new出来的”,那我们开一个脑洞:
  • Object也是函数呀,它是不是Function “new出来的”呢?答案是肯定的
  • Function也是个函数,可以看作它自己 “new出来的” 自己,自己也是一个实例

image-20211204163400465

测试:

console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Object.__proto__.__proto__ === Object.prototype); // true

console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Function); // true
console.log(Object instanceof Object); // true

面向对象案例拓展

面向对象的本质:定义不同的类,让类的实例工作

面向对象的优点:程序编写更清晰、代码结构更严密、使代码更健壮更利于维护

面向对象经常用到的场合:需要封装和复用性的场合(组件思维)

案例:红绿灯

页面上做一个红绿灯,点击红灯就变黄,点击黄灯就变绿点击绿灯就变回红灯

如何编写100个红绿灯?

使用面向对象编程,就能以**“组件化”**的思维解决大量红绿灯互相冲突的问题

面向对象编程,最重要的就是编写类

编写个TrafficLight类

请思考,红绿灯TrafficLight类有哪些属性和方法呢

属性**:自己的当前颜色color、自己的DOM元素dom**

方法:初始化init()、切换颜色changecolor()、绑定事件bindEvent()

0TXC34.jpg

0TXiv9.jpg

0TXAD1.jpg

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <style>
            #box img{
                width: 150px;
            }

        </style>
    </head>
    <body>
        <div id="box"></div>

        <script>
              // 定义红绿灯类
            function TrafficLight() {
                // 颜色属性,一开始都是红色
                // 红色1、黄色2、绿色3
                this.color = 1;
                // 调用自己的初始化方法
                this.init();
                // 绑定监听
                this.bindEvent();
            }

            TrafficLight.prototype.init = function () {
                // 创建自己的DOM
                this.dom = document.createElement("img");
                 // 设置src属性
                this.dom.src = `images/${this.color}.jpg`;
                box.appendChild(this.dom);
            };  
	
            // 绑定监听
            TrafficLight.prototype.bindEvent = function(){
                // 备份上下文,这里的this指的是JS的实例
                let self =  this;
                // 当自己的dom被点击的时候
                this.dom.onclick = function(){
                    // 当被点击的时候,调用自己的changeColor方法
                    self.changeColor();
                }
            }

            // 改变颜色
            TrafficLight.prototype.changeColor = function(){
   			// 改变自己的color属性,从而有一种“自治”的感觉,自己管理自己,不干扰别的红绿灯
                this.color++;
                if(this.color == 4){
                    this.color = 1;
                }
                // 光color属性变化没有用,还要更改自己的dom的src属性
                this.dom.src =  `images/${this.color}.jpg`;
            }


            // 得到盒子
            let box = document.getElementById("box");

            // 实例化100个
            let n = 100;

            while(n--){
                new TrafficLight()
            }

        </script>
  </body>
</html>

案例:炫彩小球案例

ball球类的属性

//属性名                 意义
x                        圆心坐标x
y                        圆心坐标y
r  					    圆半径
opacity  				透明度
color                     颜色
dom                       DOM元素

ball球类的方法

//属性名               意义  
init                  初始化
update                更新

如何实现多个小球动画

把每个小球实例都放到同一个数组中

[{小球实例},{小球实例},{小球实例}]

//只需要使用1个定时器,在每一帧遍历每个小球,调用它们的update方法
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <style>
            body {
                overflow: hidden;
                background-color: black;
            }

            .ball {
                position: absolute;
                border-radius: 50%;
                animation: changColor 1s ease infinite;
                background-color: orange;
            }

            @keyframes changColor {
                from {
                    filter: hue-rotate(0deg);
                }
                to {
                    filter: hue-rotate(360deg);
                }
            }
        </style>
    </head>
    <body>
        <script>
            function Ball(x, y) {
                // 属性x、y表示的是圆心的坐标
                this.x = x;
                this.y = y;
                //小球的背景颜色
                //this.color = "orange";
                //this.color = colorArr[parseInt(Math.random() * colorArr.length)];
                // 半径属性
                this.r = 20;
                //透明度
                this.opacity = 1;

                // 这个小球的x增量和y的增量,使用do while语句,可以防止dX和dY都是零
                do {
                    this.dX = parseInt(Math.random() * 20) - 10;
                    this.dY = parseInt(Math.random() * 20) - 10;
                } while (this.dX == 0 && this.dY == 0);

                //初始化
                this.init();

                // 把自己推入数组,注意,这里的this不是类本身,而是实例
                ballArr.push(this);
            }

            Ball.prototype.init = function () {
                // 创建自己的dom
                this.dom = document.createElement("div");
                this.dom.classList.add("ball");
                this.dom.style.width = this.r * 2 + "px";
                this.dom.style.height = this.r * 2 + "px";
                this.dom.style.top = this.y - this.r + "px";
                this.dom.style.left = this.x - this.r + "px";
                this.dom.style.backgroundColor = this.color;

                //上树
                document.body.appendChild(this.dom);
            };

            Ball.prototype.update = function () {
                // 位置改变
                this.x += this.dX;
                this.y += this.dY;
                // 半径改变
                this.r += 0.15;
                // 透明度改变
                this.opacity -= 0.01;
                this.dom.style.width = this.r * 2 + "px";
                this.dom.style.height = this.r * 2 + "px";
                this.dom.style.top = this.y - this.r + "px";
                this.dom.style.left = this.x - this.r + "px";
                this.dom.style.opacity = this.opacity;

                // 当透明度小于0的时候,就需要从数组中删除自己,DOM元素也要删掉自己
                if (this.opacity < 0) {
                    // 从数组中删除自己
                    for (let i = 0; i < ballArr.length; i++) {
                        if (ballArr[i] === this) {
                            ballArr.splice(i, 1);
                        }
                    }
                    // 还要删除自己的dom
                    document.body.removeChild(this.dom);
                }
            };

            // 把所有的小球实例都放到一个数组中
            let ballArr = [];

            // 初始颜色数组
            var colorArr = [
                "#66CCCC",
                "#CCFF66",
                "#FF99CC",
                "#FF6666",
                "#CC3399",
                "#FF6600",
            ];

            // 定时器,负责更新所有的小球实例
            setInterval(function () {
                for (let i = 0; i < ballArr.length; i++) {
                    ballArr[i].update();
                }
            }, 20);
            // 鼠标指针的监听
            document.onmousemove = function (e) {
                //new Ball(200, 200);
                // 得到鼠标指针的位置
                let x = e.clientX;
                let y = e.clientY;
                new Ball(x, y);
            };
        </script>
    </body>
</html>