九、JavaScript面向对象(1)

165 阅读27分钟

605af32009ea62fe12000200.png

对象

认识对象

JavaScript 是一种面向对象的编程语言,在 JavaScript 中几乎所有的东西都是对象。因此,要想有效的使用 JavaScript,首先需要了解对象的工作原理以及如何创建并使用对象。

我们可以将对象看作是一个属性的无序集合,每个属性都有一个名称和值(键/值对)。通过JS数据类型我们知道,数组是值的集合,每个值都有一个数字索引(从零开始,依次递增)。对象类似与数组,不同的是对象中的索引是自定义的,例如 name(姓名)、age(年龄)、sex(性别)等。

面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

那么,“对象”(object)到底是什么?我们从两个层次来理解。

(1)对象是单个实物的抽象。

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

(2)对象是一个容器,封装了属性(property)和方法(method)。

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

提示:在对象中定义的函数通常被称为方法。

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var obj = {
            name: 'xiaoming',
            age: 18,
            sex: '男',
            ['favorite-hobby']: ['唱', '跳', 'rap'],
        };

        console.log(obj.name);
        console.log(obj['favorite-hobby']);  // (3) ['唱', '跳', 'rap']
    </script>
</body>

</html>

image.png

基本数据类型与引用数据类型

JS中的变量都是保存到栈内存中的, 基本数据类型的值直接在栈内存中存储, 值与值之间是独立存在,修改一个变量不会影响其他的变量。

而对象是保存到堆内存中的,每创建一个新的对象,就会在堆内存中开辟出一个新的空间,而变量保存的是对象的内存地址(对象的引用),如果两个变量保存的是同一个对象引用,当一个通过一个变量修改属性时,另一个也会受到影响。

基本数据类型: String, Number, Boolean, Null, Undefined, Symbol, Bigint
引用数据类型: Object, Array, Function, Regexp

基本类型

基本类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配。

引用类型

引用类型:存放在堆内存中的对象的值,由地址指针与值组成,地址保存在栈中,实际的值保存在堆中,堆中的每个空间大小不一样,根据情况进行特定的分配。当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据;

如上所示:

基本数据类型值是保存在栈中,引用类型的对象引用是保存在栈中,值是保存在堆中;

栈(stack)和堆(heap)
stack为自动分配的内存空间,它由系统自动释放;而heap则是动态分配的内存,大小不定也不会自动释放。

基本数据类型和引用数据类型的储存方式有什么不同?

基本数据类型:变量名和值都储存在栈内存中,例如:

var num=10;

num变量在内存中储存如下:

2100230-20210111102727513-1251214038.png

引用数据类型:变量名储存在栈内存中,值储存在堆内存中,但是堆内存中会提供一个引用地址指向堆内存中的值,而这个地址是储存在栈内存中的,例如:

var arr=[1,2,3,4,5];

arr变量在内存中的储存如下:

v2-d232a439f3c056d0b9c28b9f88244df7_720w.jpg

JavaScript值传递与址传递

基本类型与引用类型最大的区别实际就是传值与传址的区别

值传递:基本类型采用的是值传递。

2100230-20210108164051703-2035749280.png 址传递:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量。

2100230-20210108164238306-204983428.png 分析:由于a和b都是引用类型,采用的是址传递,即a将地址传递给b,那么a和b必然指向同一个地址(引用类型的地址存放在栈内存中),而这个地址都指向了堆内存中引用类型的值。当b改变了这个值的同时,因为a的地址也指向了这个值,故a的值也跟着变化。

那么如何解决上面出现的问题,就是使用浅拷贝或者深拷贝了。

总结

声明变量时不同的内存分配: 

基本类型:存储在栈(stack)中的简单数据段,它们的值直接存储在变量访问的位置。这是因为基本类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中,这样存储便于迅速查询变量的值。

引用类型:存储在堆(heap)中的对象,存储在栈中的值是一个指针(point)用于指向存储对象的内存地址,这是因为引用类型值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反存在栈中的值是该对象地址而地址的大小是固定的,所以把它存储在栈中对变量性能无任何影响。

不同的内存分配机制也带来了不同的访问机制

在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先访问内存栈得到这个对象在内存堆中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问,而基本类型的值则是直接内存栈中。  

复制变量时的不同  

基本类型:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。 

引用类型:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量)多了一个指针。

对象语法

image.png

键名命名

image.png

属性访问

image.png 如果读取对象中没有的属性,不会报错而是会返回undefined

image.png

image.png 当属性名作为变量的时候,一定要使用[ ]来访问,不能打点的形式访问

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        let obj = {
            a: 1,
            'c-d': 2,
            'e-f-g': 3,
        }

        console.log(obj.a);
        console.log(obj['c-d']);
        console.log(obj['e-f-g']);

        // 将属性值赋值给变量
        let h = ['a'];
        console.log(obj[h]);
    </script>
</body>

</html>

image.png

属性的更改

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        let obj = {
            a: 1,
            b: 2,
        };

        obj.a = 6;
        console.log(obj.a);
        obj.a++;
        console.log(obj.a);
    </script>
</body>

</html>

image.png

属性的创建

image.png

image.png

属性的删除

image.png

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>对象属性的删除</title>
</head>

<body>
    <script>
        var obj = {
            num1: 1,
            num2: 2
        };
        delete obj.num1;
        console.log(obj);
    </script>
</body>

</html>

image.png

对象的方法

image.png

image.png

image.png

image.png 运行结果如下: image.png

练习

image.png

对象的遍历

for···in···

image.png

image.png k就相当于每个对象的键(属性名),将每个键作为变量赋给了变量k

for in 循环的固定写法,k代表的是循环变量,可以自定义这个变量名,需特殊记忆

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var obj = {
            a: 1,
            b: 2,
            c: 3,
        }

        for (var k in obj) {
            console.log(`对象obj的属性${k}值为${obj[k]}`);
        }
    </script>
</body>

</html>

image.png

练习

image.png

对象的深浅克隆

image.png

image.png 看下面的例子:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var obj1 = {
            a: 3,
            b: 4,
            c: 5
        };
        var obj2 = {
            a: 3,
            b: 4,
            c: 5
        };

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

        console.log({} == {});          //false
        console.log({} === {});         //false
    </script>
</body>

</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var obj1 = {
            a: 10,
        };

        var obj2 = obj1;
        obj1.a++;
        console.log(obj2);   // {a: 11}
    </script>
</body>

</html>

对象的浅克隆

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        // 对象浅克隆
        var obj1 = {
            a: 4,
            b: 5,
            c: 6,
            d: [7, 8, 9],
        };

        var obj2 = {};
        
        for (var k in obj1) {
            obj2[k] = obj1[k];
        }

        console.log(obj2);
    </script>
</body>

</html>

image.png 注意:浅克隆是不能克隆引用数据类型的,它只管一层

对于引用类型的数据(比如:数组、对象)来说,如果使用 var obj2 = obj1 这样的语法将obj1赋给obj2的话,那么只是将obj1的引用地址赋给了obj2,此时obj1与obj2指向的是同一个引用地址,也就是同一个值。

所以,如果obj1的值发生改变,那么obj2肯定也会改变(obj2与obj1指向的是同一个值)

对象的深克隆

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var obj1 = {
            a: 1,
            b: 2,
            c: [3, 4, {
                d: 5,
                e: 6
            }]
        };

        var obj2;

        const deepClone = (o) => {
            if (Array.isArray(o)) {
                var ret = [];
                for (var i = 0; i < o.length; i++) {
                    ret.push(deepClone(o[i]));
                }
            } else if (typeof o == 'object') {
                var ret = {};
                for (var k in o) {
                    ret[k] = deepClone(o[k]);
                }
            } else {
                var ret = o;
            }
            return ret;
        };

        obj2 = deepClone(obj1);

        console.log(obj2);
        console.log(obj1 == obj2);
    </script>
</body>

</html>

image.png 注意:这里定义了三个ret变量,

是不冲突的,如果第一个if判断不成立,它会去走第二个判断else if ,如果第二判断不成立会继续往下走,走下面的else

上下文规则

认识上下文

image.png

image.png

image.png

image.png

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var xiaoming = {
            nickname: '小明',
            age: 18,
            fu: function () {
                console.log(`我是${this.nickname}${this.age}岁了`);
            },
        };

        xiaoming.fu();

        var fun = xiaoming.fu;
        fun();
    </script>
</body>

</html>

image.png

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

image.png 示例

image.png 答案:不知道!函数只有被调用,它的上下文才能被确定

示例 image.png 这个的结果就是正常的 3

示例

image.png 结果是NaN

解析

首先需要知道的是:函数的上下文(即this指向)由函数调用方式决定。

所以,此时需要判断一下函数fn是如何调用的,从而确定函数fn中的this指向以及thia.a与this.b的值。

从这段代码中可以看出:函数fn是以圆括号的方式调用的,即fn(),所以函数fn中的this指向window对象,如图

image.png 而在window对象中,不存在属性a和属性b,所以this.a为undefined,this.b也为undefined,如图

image.png 两个undefined相加,结果为NaN(非数字),如图

image.png undefined + undefined = NaN image.png

上下文规则(1)

image.png

image.png

面试题1

image.png 答案:

image.png

面试题2

image.png 答案:

image.png

面试题3

image.png 答案:

image.png 解析:

outer函数返回了一个对象,此时outer( )就相当于返回的一个对象{ },通过对象.方法()的规则

image.png

面试题4

image.png 答案:

image.png

上下文规则(2)

image.png

面试题1

image.png 答案:

image.png 解析:

根据上下文规则2:以圆括号的形式调用函数,则函数的上下文(this)是window对象。

这段代码中,fn是以圆括号的形式调用的,即:fn(),所以函数fn中的this指向window,如图

image.png 所以,this.a和this.b表示window对象中的属性a和属性b,而不是obj1对象中的属性a和属性b。

又因为在全局作用域中定义了变量a和变量b,在全局作用域中定义的变量会默认为window对象的属性,如图

image.png 因此,综上可得:this.a = 3,this.b = 4,相加得7,如图

image.png

面试题2

image.png 答案:

image.png 解析:

这段代码涉及了两个上下文规则,需要一个一个来看:

1、首先来看obj1对象中的属性b,属性b的值为fun函数的返回结果【b:fun() 】,在fun函数中 return this.a + this.b 表示函数的返回结果为:this.a + this.b 的和

此时fun函数是以圆括号的形式调用的【即fun(),此为规则2】,所以fun函数中的this指向window对象,this.a和this.b是window对象的属性a和属性b,如图

615185e5091cea6814470553.png 2、经过第一步之后,obj1对象变为如下:

6151866a09b081a106730452.png 然后执行var result = obj1.fun(),表示调用obj1对象中的fun函数,并把返回结果存放到变量result中。

此时fun函数的调用方式为:对象打点调用它的方法函数【即obj1.fun(),此为规则1】,所以fun函数的上下文(this)是这个打点的对象,即obj1对象,fun函数中的this.a和this.b也是obj1对象的属性,如图

6151897d099c7ef212130555.png

单选题

image.png 解析: image.png

image.png

上下文规则(3)

image.png

面试题1

image.png 答案:

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>函数上下文规则3</title>
</head>

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

        arr[3]();
    </script>
</body>

</html>

image.png 类数组对象:

image.png

面试题2

image.png 答案:

image.png 解析:

image.png

单选题

image.png 解析:

一般访问对象中的某个属性,通常需要打点调用,比如:obj.a。

此时数组中的a是访问变量的写法(c: [1, a , function() {}]),而不是访问对象属性的写法,因此a是全局变量a。

上下文规则(4)

image.png

面试题1

image.png 答案:

image.png 解析:

函数中的this,只有在调用的时候,才能确定具体的指向。 代码从上往下执行,先解析变量a,再解析变量obj:

6152797609818aaf05590158.png 对于obj而言,它的fun方法的具体值是自执行函数(自动执行的函数)的返回值:

6152792a097bc8ff09800428.png 因此自执行函数中的this可以确定指向,根据规则4,该this指向window,所以如下位置的a是1:

615276d309b4ee9809620495.png 而return后面的函数中的this,此时无法确定指向,因为它所在的函数还没有调用:

615279a80931245e09460395.png 代码继续往下执行,会执行obj.fun(),由于fun的值是return后面的函数:

61527a1b092cc31510570566.png 所以如下两种形式等价:

61527ab8094a462306780527.png 因此obj.fun( ),就是执行下图中选中的函数:

61527ace0931fd8604810188.png 此时,选中部分函数中的this,指向obj(调用形式是obj.fun( ),fun有具体的调用者obj,因此指向obj),因此this.a就是obj.a,也就是2。因此最终的结果是3:

61527bd809c9087110530497.png

上下文规则(5)

image.png

题目1

image.png 答案: image.png

题目2

image.png 答案:

image.png 解析:

因为setTimeout调用的是外部函数function,而obj.fun()是外部函数调用的,不是setTimeout调用的。

只有setTimeout和setInterval调用的是外部函数的时候,this才会指向window。

上下文规则(6)

image.png

案例1

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box1,
        .box2,
        .box3 {
            width: 100px;
            height: 100px;
            margin-bottom: 10px;
            border: 2px solid #bfa;
        }
    </style>
</head>

<body>
    <div id="box1" class="box1"></div>
    <div id="box2" class="box2"></div>
    <div id="box3" class="box3"></div>

    <script>
        var boxs = document.getElementsByTagName('div');
        for (var i = 0; i < boxs.length; i++) {
            boxs[i].onclick = function () {
                this.style.backgroundColor = 'red';     // 注意这里用的this
            };
        }
    </script>
</body>

</html>

动画.gif

案例2

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box1,
        .box2,
        .box3 {
            width: 100px;
            height: 100px;
            margin-bottom: 10px;
            border: 2px solid #bfa;
        }
    </style>
</head>

<body>
    <div id="box1" class="box1"></div>
    <div id="box2" class="box2"></div>
    <div id="box3" class="box3"></div>

    <script>
        var boxs = document.getElementsByTagName('div');
        for (var i = 0; i < boxs.length; i++) {
            boxs[i].onclick = function () {
                var self = this;        // //备份上下文
                setTimeout(function () {
                    self.style.backgroundColor = 'red';
                }, 2000);
            };
        }
    </script>
</body>

</html>

动画.gif

备份上下文

提前备份一个this,以防过后的this指向发生改变。

可以使用变量self、_self、that来表示备份的上下文

image.png

call 和 apply

image.png

image.png

  • 将某个函数的上下文设置成某个对象

call

函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true

上面代码中,全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f

call方法的参数,应该是一个对象。如果参数为空、nullundefined,则默认传入全局对象。

var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

上面代码中,a函数中的this关键字,如果指向全局对象,返回结果为123。如果使用call方法将this关键字指向obj对象,返回结果为456。可以看到,如果call方法没有参数,或者参数为nullundefined,则等同于指向全局对象。

如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法。

var f = function () {
  return this;
};

f.call(5)
// Number {[[PrimitiveValue]]: 5}

上面代码中,call的参数为5,不是对象,会被自动转成包装对象(Number的实例),绑定f内部的this

call方法还可以接受多个参数。

func.call(thisValue, arg1, arg2, ...)

call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。

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

add.call(this, 1, 2) // 3

上面代码中,call方法指定函数add内部的this绑定当前环境(对象),并且参数为12,因此函数add运行后得到3

简单示例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function sum() {
            console.log(this.math + this.english + this.Chinese);
        };

        var zhangSan = {
            math: 60,
            english: 60,
            Chinese: 60,
        };

        sum.call(zhangSan);
    </script>
</body>

</html>

image.png

apply

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

func.apply(thisValue, [arg1, arg2, ...])

apply方法的第一个参数也是this所要指向的那个对象,如果设为nullundefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。

function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

上面代码中,f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。

利用这一点,可以做一些有趣的应用。

找出数组最大元素

JavaScript 不提供找出数组中最大元素的函数。结合使用apply方法和Math.max方法,就可以返回数组的最大元素。

var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15

call和apply区别

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function sum(b1, b2) {
            console.log(this.math + this.english + this.Chinese + b1 + b2);
        };

        var xiaoming = {
            math: 60,
            english: 60,
            Chinese: 60,
        };

        sum.call(xiaoming, 10, 20);
        sum.apply(xiaoming, [20, 20]);
    </script>
</body>

</html>

image.png

思考:

此时应该使用call还是apply? image.png

解析: image.png

上下文规则总结

image.png

单选题

image.png

多选题

image.png

构造函数与类

面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。

典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

构造函数就是一个普通的函数,但具有自己的特征和用法。

var Vehicle = function () {
  this.price = 1000;
};

上面代码中,Vehicle就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写。

构造函数的特点有两个。

  • 函数体内部使用了this关键字,代表了所要生成的对象实例。
  • 生成对象的时候,必须使用new命令。

下面先介绍new命令。

new命令的作用,就是执行构造函数,返回一个实例对象。

var Vehicle = function () {
  this.price = 1000;
};

var v = new Vehicle();
v.price // 1000

上面代码通过new命令,让构造函数Vehicle生成一个实例对象,保存在变量v中。这个新生成的实例对象,从构造函数Vehicle得到了price属性。new命令执行时,构造函数内部的this,就代表了新生成的实例对象,this.price表示实例对象有一个price属性,值是1000。

使用new命令时,根据需要,构造函数也可以接受参数。

var Vehicle = function (p) {
  this.price = p;
};

var v = new Vehicle(500);

new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示这里是函数调用,推荐使用括号。

// 推荐的写法
var v = new Vehicle();
// 不推荐的写法
var v = new Vehicle;

一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?

这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果。

var Vehicle = function (){
  this.price = 1000;
};

var v = Vehicle();
v // undefined
price // 1000

上面代码中,调用Vehicle构造函数时,忘了加上new命令。结果,变量v变成了undefined,而price属性变成了全局变量。因此,应该非常小心,避免不使用new命令、直接调用构造函数。

为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。

function Fubar(foo, bar){
  'use strict';
  this._foo = foo;
  this._bar = bar;
}

Fubar()
// TypeError: Cannot set property '_foo' of undefined

上面代码的Fubar为构造函数,use strict命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript 不允许对undefined添加属性)。

用new调用函数的四步走

image.png

image.png

image.png

image.png

image.png

image.png 使用new命令时,它后面的函数依次执行下面的步骤。

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码。

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

var Vehicle = function () {
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000
// false

上面代码中,构造函数Vehiclereturn语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。

但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。

var Vehicle = function (){
  this.price = 1000;
  return { price: 2000 };
};

(new Vehicle()).price
// 2000

上面代码中,构造函数Vehiclereturn语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。

另一方面,如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象。

function getMessage() {
  return 'this is a message';
}

var msg = new getMessage();

msg // {}
typeof msg // "object"

上面代码中,getMessage是一个普通函数,返回一个字符串。对它使用new命令,会得到一个空对象。这是因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象。本例中,return语句返回的是字符串,所以new命令就忽略了该语句。

上下文规则总结

image.png

构造函数

image.png

image.png

image.png

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

        var xiaoming = new People('小明', 18, '男');

        console.log(xiaoming);
    </script>
</body>

</html>

image.png this.name=xxx就是给this指向的对象添加了一个name属性。name属性的属性值可以通过形参传入:

image.png

构造函数命名

image.png 注意:

image.png

image.png

对象添加方法

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function People(name, age, sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
            this.sayHello = function () {
                console.log('我是' + this.name + ',今年' + this.age + '岁');
            };
        };

        var xiaoming = new People('小明', 18, '男');
        xiaoming.sayHello();
    </script>
</body>

</html>

image.png

类和实例

js中没有真正的类,只是通过构造函数的方式模拟其他语言中类的部分功能。

es6中新增的class,并不是传统的类”,它只是语法糖。新的class写法只是让构造函数的写法更加清晰、更像面向对象编程的语法而已。

image.png

image.png

image.png

image.png

image.png

image.png

image.png 可以理解为一切皆对象,js中的构造函数可以类比于面向对象语言中的类,写法有些类似,本质上还是有区别

原型原型链

构造函数的缺点

JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。

function Cat (name, color) {
  this.name = name;
  this.color = color;
}

var cat1 = new Cat('大毛', '白色');

cat1.name // '大毛'
cat1.color // '白色'

上面代码中,Cat函数是一个构造函数,函数内部定义了name属性和color属性,所有实例对象(上例是cat1)都会生成这两个属性,即这两个属性会定义在实例对象上面。

通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.meow = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow
// false

上面代码中,cat1cat2是同一个构造函数的两个实例,它们都具有meow方法。由于meow方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow方法。这既没有必要,又浪费系统资源,因为所有meow方法都是同样的行为,完全应该共享。

这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。

prototype

image.png

image.png

image.png JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个prototype属性,指向一个对象。

function f() {}
typeof f.prototype // "object"

上面代码中,函数f默认具有prototype属性,指向一个对象。

对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

上面代码中,构造函数Animalprototype属性,就是实例对象cat1cat2的原型对象。原型对象上添加一个color属性,结果,实例对象都共享了该属性。

原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

上面代码中,原型对象的color属性的值变为yellow,两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性,都是读取原型对象的color属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。

cat1.color = 'black';

cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';

上面代码中,实例对象cat1color属性改为black,就使得它不再去原型对象读取color属性,后者的值依然为yellow

总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。

Animal.prototype.walk = function () {
  console.log(this.name + ' is walking');
};

上面代码中,Animal.prototype对象上面定义了一个walk方法,这个方法将可以在所有Animal实例对象上面调用。

__proto__

这个属性不是w3里面规定的属性,但是是每个浏览器都有的

构造函数的prototype属性就是实例的原型(__proto__)

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

        var xiaoming = new People('小明', 20);

        console.log(xiaoming.__proto__ === People.prototype);
    </script>
</body>

</html>

image.png

constructor 属性

prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

function P() {}
P.prototype.constructor === P // true

由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承。

function P() {}
var p = new P();

p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false

上面代码中,p是构造函数P的实例对象,但是p自身没有constructor属性,该属性其实是读取原型链上面的P.prototype.constructor属性。

constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

function F() {};
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false

上面代码中,constructor属性确定了实例对象f的构造函数是F,而不是RegExp

原型链查找

image.png

image.png

image.png JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……

如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOftoString方法的原因,因为这是从Object.prototype继承的。

那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

Object.getPrototypeOf(Object.prototype)
// null

上面代码表示,Object.prototype对象的原型是null,由于null没有任何属性,所以原型链到此为止。Object.getPrototypeOf方法返回参数对象的原型,具体介绍请看后文。

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。

注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

一个对象如果打点调用一个属性,在这个对象的身上并没有这个属性,它会寻找这个对象的原型上是否有这个属性

原型链的遮蔽效应

实例化后的对象,调用属性或者方法时,会先在自己实例中找,如果没有,再去找原型上的。

如果实例上定义了,就不会找原型上的,这样输出的就是实例上的值,遮蔽掉了原型上的值。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>原型链的遮蔽效应</title>
</head>

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

        People.prototype.score = '80分';
        var xiaoming = new People('小明', 20, '男');
        var tom = new People('tom', 18, '男');
        tom.score = '100分';
        console.log(xiaoming.score);    //80分
        console.log(tom.score);    //100分
        console.log(xiaoming);
        console.log(tom);      
    </script>
</body>

</html>

如果tom实例对象中没有sorce属性(只有原型上的sorce属性),则结果如下: image.png 若tom实例对象中有sorce属性,则结果如下: image.png 注意事项:

  1. 通过prototype.属性的方式添加属性,是将属性添加到构造函数的原型中,而原型中的属性 所有实例对象都可以通过原型链访问到,如图

image.png 而通过实例.属性的方式添加属性,是将属性添加给这个实例对象,只有这个实例本身有该属性,其他实例没有,如图

image.png 通常情况下,很少会通过prototype.属性的方式添加属性,而是将属性直接添加到构造函数中,将方法添加到原型中(可以节约内存),如图

image.png 2. 在原型上添加属性,就是将属性添加给构造函数的原型,当实例对象访问时,是通过原型链访问的。

在构造函数中添加属性,则是将属性直接添加给实例,当实例对象访问时,可以直接访问到。

如果属性重名的话,优先访问实例上的属性,如图

image.png

hasOwnProperty

image.png 不包括构造函数添加的原型属性和方法

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

        People.prototype.sex = '男';
        People.prototype.sayHello = function () {
            console.log(this.name + 'hello');
        };

        var xiaoming = new People('小明', 20);
        
        console.log(xiaoming.hasOwnProperty('name'));
        console.log(xiaoming.hasOwnProperty('age'));
        console.log(xiaoming.hasOwnProperty('sex'));
        console.log(xiaoming.hasOwnProperty('sayHello'));
    </script>
</body>

</html>

image.png

in 运算符

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

        People.prototype.sex = '男';
        People.prototype.sayHello = function () {
            console.log(this.name + 'hello');
        };

        var xiaoming = new People('小明', 20);

        console.log('name' in xiaoming);
        console.log('age' in xiaoming);
        console.log('sex' in xiaoming);
        console.log('sayHello' in xiaoming);
    </script>
</body>

</html>

image.png

在prototype上添加方法

image.png

image.png

image.png 他们三个实例对象的方法,不是同一个方法

image.png

image.png

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

        People.prototype.sayHello = function () {
            console.log(this.name + 'hello');
        };

        var zhangSan = new People('张三', 20);
        var liSi = new People('李四', 18);

        console.log(zhangSan.sayHello === liSi.sayHello);    //说明是同一个内存的函数
    </script>
</body>

</html>

image.png

原型链的终点

在JS中,Object就是最顶层的构造函数

image.png

image.png

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function People() { };
        var xiaoming = new People();

        console.log(xiaoming.__proto__ === People.prototype);
        console.log(xiaoming.__proto__.__proto__ === Object.prototype);   //true  说明xiaoming这个实例对象原型的原型 是 Object.prototype

        console.log(Object.prototype.__proto__);  // null 说明Object.prototype没有原型了,Object.prototype是原型链的终点

        console.log(Object.prototype.hasOwnProperty('toString'));
        console.log(Object.prototype.hasOwnProperty('hasOwnProperty'));
    </script>
</body>

</html>

image.png

数组的原型链

image.png

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数组的原型链</title>
</head>

<body>
    <script>
        var arr = [1, 2];
        console.log(arr.__proto__ === Array.prototype);
        console.log(arr.__proto__.__proto__ === Object.prototype);
        console.log(arr.__proto__.__proto__ === Array.prototype.__proto__);

        console.log(arr.__proto__.hasOwnProperty('push'));
        console.log(arr.__proto__.hasOwnProperty('splice'));
    </script>
</body>

</html>

image.png

继承相关知识请看下一篇 微信图片_20220922141624.png