# 面向对象

# 对象简介

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

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

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

属性是否加引号

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

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

属性的访问

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

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

console.log(obj.name); //云牧
console.log(obj.age); //18
console.log(obj.hobbies); //["打篮球", "编程"]
1
2
3
4
5
6
7
8
9
10

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

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

console.log(obj["na-me"]);
1
2
3
4
5

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

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

属性的更改

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

let obj = {
    a: 10
};
obj.a = 30;
obj.a++;
1
2
3
4
5

属性的创建

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

let obj = {
    a: 10
};
obj.b = 40;
1
2
3
4

属性的删除

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

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

# 对象的方法

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

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

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

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

xiaoming.sayHello() ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

# 对象的遍历

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

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

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

# 对象的深浅克隆

wvJUne.png (opens new window)

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

不能用 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}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 浅克隆

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

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就证明了数组是同一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 深克隆

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

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 上下文对象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(); //直接圆括号调用这个函数而不是对象打点调用了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

0IkSwd.png (opens new window)

得出一个结论

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

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

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

yunmu.sayHello();
1

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

let say = yunmu.sayHello; 
say();
1
2

题目

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








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

//不知道  因为它的上下文只有当被调用上下文才确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

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

# 规则1

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

对象.方法();
1

# 题目1

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

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

    obj.fn(); //构成对象.方法()的形式,适用规则1
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

运行结果是

99
1

# 题目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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行结果

7
1

# 题目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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

运行结果

77
1

# 题目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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果

7
1

# 规则2

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

函数();
1

# 题目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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果

7
1

# 题目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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行结果

6
1

# 规则3

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

数据[下标]();
1

# 题目1

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

    arr[3]();  //适用规则3
</script>
1
2
3
4
5
6
7
8
9
10
11
12

运行结果

A
1

# 题目2

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

运行结果

B
1

# 规则4

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

(function(){})();
1

# 题目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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

运行结果

3
1

# 规则5

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

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

# 题目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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

运行结果

7
3
1
2

# 规则6

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

DOM元素.onclick = function () {}
1

# 小案例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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 小案例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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# call和apply和bind

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

函数.call(上下文);
函数.apply(上下文);
1
2
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 到底使用call还是apply?

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

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

    fn1(11, 22);
</script>
1
2
3
4
5
6
7
8
9
10
11

0Idmk9.png (opens new window)

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

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

function fn(){
    console.log("fn执行了");
    console.log(this);
}
// fn.call()函数会自执行 返回undefined 
document.onclick  = fn.call(xiyan);
1
2
3
4
5
6
7
8

现在我们既要改变函数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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 构造函数与类

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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

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

//工厂模式
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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

new.函数()
1

你可能知道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>
1
2
3
4
5
6
7
8
9

相当于

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

    let obj = new fn(); //返回的上下文对象被变量obj接收
    console.log(obj);
</script>
1
2
3
4
5
6
7
8
9
10
11
12

例子

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

0IBnYj.png (opens new window)

# 构造函数

# 什么是构造函数

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

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

运行结果如下图

0TnZvR.png (opens new window)

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

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

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

请判断对错:

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

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

者的习惯约定

如果不用new调用构造函数

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

# 尝试为对象添加方法

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

结果如下

0TnWZV.png (opens new window)

# 类和实例

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构造函数得到对象的过程,称之为实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

类好比是蓝图

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

0I69MR.png (opens new window)

实例是具体的对象

0I6xTf.png (opens new window)

0IckXn.png (opens new window)

0IcV00.png (opens new window)

比如

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

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

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

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

# 原型和原型链

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

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

0IcX34.png (opens new window)

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

console.log(fn.prototype); 
console.log(typeof fn.prototype);
console.log(fn.prototype.constructor === fn); 
1
2
3
4
5
6
7

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

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

0IRSk6.png (opens new window)

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
1
2
3
4
5
6
7
8
9

# 原型链查找

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); //实例可以打点访问原型的属性和方法
1
2
3
4
5
6
7
8
9
10
11
12

0IhjgO.png (opens new window)

遮蔽效应

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); //中国
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 在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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

方法要写到prototype上

0IIOCq.md.png (opens new window)

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

运行结果如下图

0TMpDI.png (opens new window)

# 原型链的终点

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

image-20201014233021503

# 关于数组的原型链

0IO7h4.png (opens new window)

<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>
1
2
3
4
5
6
7
8
9

var a =其实是var a = new Object()的语法糖

var a =[]其实是var a = new Array()的语法糖

function Foo(){..}其实是var Foo = new Function(...)

使用instanceof 判断一个函数是否是一个变量的构造函数

判断一个变量是否为“数组 变量 instanceof Array

# 继承

先来看两幅图

两个无关的类

0TQfX9.png (opens new window)

People类和Student类的关系

0TlAXj.png (opens new window)

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

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

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

继承

继承描述了两个类之间的“is a kind of”关系,比如学生“是一种”人,所以人类和学生类之间就构成继承关系

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

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

0T1SKJ.png (opens new window)

更多的继承关系举例

0T12L9.png (opens new window)

# 1.通过原型链实现继承

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

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

0TUTr8.png (opens new window)

<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");
    };

    //子类 学生类
    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.scholl},我${this.age}`);
    }

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

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

    let yunmu = new People("云牧", 18, "男");
    yunmu.sayHello();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

运行结果如下

0TtnsS.png (opens new window)

# 2. 构造函数继承

方法都在构造函数中定义, 只能继承父类的实例属性和方法,不能继承原型属性/方法,无法实现函

数复用,每个子类都有父类实例函数的副本,影响性能

function People(name){
    this.name = name;
}
People.prototype.sayHello = function(){
    alert("你好我是" + this.name);
}

function Student(name,xuehao){
    //  核心语句
    People.call(this,name);
    // 这样就会在新parent对象上执行Person构造函数中定义的所有对象初始化代码
    this.xuehao = xuehao;
}

Student.prototype.study = function(){
    alert("好好学习,天天向上");
}

let xiaohong = new Student("小红",1001);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3. 组合继承

就是将原型链继承和构造函数继承组合在一起;继承两个优点

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

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

function People(name){
    this.name = name;
}
People.prototype.sayHello = function(){
    alert("你好我是" + this.name);
}

function Student(name,xuehao){
    People.call(this,name);
    this.xuehao = xuehao;
}

//核心语句,继承的实现全靠这条语句了:
Student.prototype = new People('大明');

Student.prototype.study = function(){
    alert("好好学习,天天向上");
}

let xiaohong = new Student("小红",1001);

xiaohong.sayHello();

//调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4. 寄生组合继承

通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例

方法/属性,避免的组合继承的缺点

function People(name){
    this.name = name;
}
People.prototype.sayHello = function(){
    alert("你好我是" + this.name);
}

function Student(name,xuehao){
    People.call(this,name);
    this.xuehao = xuehao;
}

//核心语句,继承的实现全靠这条语句了:
function Fn(){}
Fn.prototype = People.prototype
Student.prototype = new Fn();

Student.prototype.study = function(){
    alert("好好学习,天天向上");
}

let xiaohong = new Student("小红",1001);

xiaohong.sayHello();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 5. 圣杯模式

function inherit(People,Student){
    function Fn(){}
    Fn.prototype = People.prototype
    Student.prototype = new Fn();
    Student.prototype.constructor = Student;
    Student.prototype.parent = People;
}
inherit(People,Student )
1
2
3
4
5
6
7
8

# 面向对象案例拓展

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

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

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

# 案例:红绿灯

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

如何编写100个红绿灯?

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

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

编写个TrafficLight类

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

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

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

0TXC34.jpg (opens new window)

0TXiv9.jpg (opens new window)

0TXAD1.jpg (opens new window)

<!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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

# 案例:炫彩小球案例

ball球类的属性

//属性名                 意义
x                        圆心坐标x
y                        圆心坐标y
r  					    圆半径
opacity  				透明度
color                     颜色
dom                       DOM元素
1
2
3
4
5
6
7

ball球类的方法

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

如何实现多个小球动画

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

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

//只需要使用1个定时器,在每一帧遍历每个小球,调用它们的update方法
1
2
3
<!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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127