更新数据类型以及原型链课件
This commit is contained in:
parent
fc9d3870d6
commit
ff1a2720af
257
02. JS中的数据类型/课件资料/JS中的数据类型.md
Normal file
257
02. JS中的数据类型/课件资料/JS中的数据类型.md
Normal file
@ -0,0 +1,257 @@
|
||||
# JS中的数据类型
|
||||
|
||||
>面试题:JS 中的数据类型有哪些?基本类型和引用类型的区别是什么?
|
||||
|
||||
- 简单值和复杂值
|
||||
- 两者之间本质区别
|
||||
- 两者之间行为区别
|
||||
|
||||
## 简单值和复杂值
|
||||
|
||||
JS 中的数据类型就分为**两大类**:
|
||||
|
||||
- 简单值(基本类型、原始类型)
|
||||
- 复杂值(引用值、引用类型)
|
||||
|
||||
|
||||
|
||||
### 1. 简单值
|
||||
|
||||
一共**有 7 种**:
|
||||
|
||||
- number:数字
|
||||
- string:字符串
|
||||
- boolean:布尔值
|
||||
- undefined:未定义
|
||||
- null:空
|
||||
- symbol:符号
|
||||
- bigint:大数
|
||||
|
||||
所谓简单值,是因为**这些类型的值,无法再继续往下拆分**。
|
||||
|
||||
**注意点1: symbol 和 bigint**
|
||||
|
||||
这两种数据类型从 ES6 开始新增的。
|
||||
|
||||
symbol 这种类型主要用于**创建唯一的标识符**。symbol 的值是唯一且不可变的,适用于作为对象属性的键,以及保证不会与其他属性键发生冲突,特别是在多人合作的大型项目中或者当你使用第三方库的时候。
|
||||
|
||||
bigint 是一种新增的基本数据类型,它于 ECMAScript 2020(ES11)中被正式添加到语言标准中。bigint 数据类型用于表示大于Number.MAX_SAFE_INTEGER(即 `2^53 - 1`)或小于 Number.MIN_SAFE_INTEGER(即`-2^53 + 1`)的整数。这个类型**提供了一种在 JS 中安全处理非常大的整数的方法**,这在之前的 JS 版本中是不可能的。这种类型非常适合于用在金融、科学计算和加密等领域。
|
||||
|
||||
|
||||
|
||||
**注意点2: null 和 undefined**
|
||||
|
||||
>面试题1:为什么 null 的数据类型打印出来为 object ?
|
||||
|
||||
```js
|
||||
console.log(typeof null); // object
|
||||
console.log(typeof undefined); // undefined
|
||||
```
|
||||
|
||||
这其实是 JS 从第一个版本开始时,**设计上的一个遗留问题**。最初的 JS 语言实现是在 1995 年由 Brendan Eich 在 Netscape Navigator 中设计的。在 JS 最初的版本中,**数据类型是使用底层的位模式来标识的,每种数据类型的前几位是用来表示类型信息的**。例如,**对象的类型标记通常以 00 开头**,而由于一个历史错误,**null 被表示为全零(00000000)**,这就使得 null 的类型检查结果与对象一致。
|
||||
|
||||
虽然这个行为在技术上是不正确的(因为 null 既不是对象也不包含任何属性),但改变这个行为可能会破坏大量现存的 Web 页面和应用。因此,尽管这是一个众所周知的问题,但由于向后兼容性的考虑,这个设计决策一直未被修改。
|
||||
|
||||
不仅没有修改,这个行为目前还被 ECMAScript 标准所采纳,成为了规范的一部分,所有遵循 ECMAScript 标准的 JS 实现都默认在 typeof null 时返回 object.
|
||||
|
||||
>面试题2:为什么 undefined 和 null 明明是两种基础数据类型,但 undefined == null 返回的是 true ?
|
||||
|
||||
这个问题其实也是一个历史问题。众所周知,JS 是借鉴了在当时很多已有的语言的一个产物。其中关于“无”这个概念,JS 就是借鉴的 Java,使用 null 来表示“无”的意思,而<u>根据 C 语言的传统,null 被设计成可以自动转为 0</u>.
|
||||
|
||||
但是 Brendan Eich 觉得这么做还不够,主要是因为如下两个原因:
|
||||
|
||||
1. 由于前面所介绍的设计上的失误,获取 null 的数据类型会得到一个 obect,这在开发上会带来一些未知的问题。
|
||||
2. JS 在设计之初就是弱类型语言,当发生数据类型不匹配的时候,往往会自动数据类型转换或者静默失败,null 自动转为 0 的话也很不容易发现错误。
|
||||
|
||||
基于上面的这些理由,Brendan Eich 又设计出来了 undefined. 也就是说,undefined 实际上是为了填补 null 所带来的坑。
|
||||
|
||||
```js
|
||||
console.log(null + 1); // 1
|
||||
console.log(undefined + 1); // NaN
|
||||
```
|
||||
|
||||
目前来讲,关于 null 和 undefined 主要区别总结如下:
|
||||
|
||||
- null:从语义上来讲就是表示对象的 “无”
|
||||
- 转为数值时会被转换为 0
|
||||
- 作为原型链的终点
|
||||
- undefined:从语义上来讲就是表示简单值的“无”
|
||||
- 转为数值为 NaN
|
||||
- 变量声明了没有赋值,那么默认值为 undefined.
|
||||
- 调用函数没有提供要求的参数,那么该参数就是 undefined
|
||||
- 函数没有返回值的时候,默认返回 undefined.
|
||||
|
||||
|
||||
|
||||
### 2. 复杂值
|
||||
|
||||
**复杂值就一种**:object
|
||||
|
||||
之所以被称之为复杂值,就是**因为这种类型的值可以继续往下拆分,分为多个简单值或者复杂值**。
|
||||
|
||||
```js
|
||||
const obj = {
|
||||
name: "张三",
|
||||
age: 18,
|
||||
scores: {
|
||||
"htmlScore": 99,
|
||||
"cssScore": 95
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**像数组、函数、正则这些统统都是对象类型,属于复杂值**
|
||||
|
||||
```js
|
||||
console.log(typeof []); // object
|
||||
console.log(typeof function () {}); // function
|
||||
console.log(typeof /abc/); // object
|
||||
```
|
||||
|
||||
**函数的本质也是对象。**
|
||||
|
||||
```js
|
||||
function func() {}
|
||||
// 该函数我是可以正常添加属性和方法的
|
||||
func.a = 1; // 添加了一个属性
|
||||
func.test = function () {
|
||||
console.log("this is a test function");
|
||||
}; // 添加了一个方法
|
||||
console.log(func.a); // 1
|
||||
func.test(); // this is a test function
|
||||
```
|
||||
|
||||
在函数内部有一个特别的内部属性 `[[Call]]`,这个是属于内部代码,开发者层面是没有办法调用的。但是**有了这个属性之后,表示这个对象是可以被调用。**
|
||||
|
||||
因为函数是可调用的对象,为了区分 **普通对象** 和 **函数对象**,因此当我们使用 typeof 操作符检测一个函数时,它返回的是 function。
|
||||
|
||||
也正因为这种设计,所以 JS 中能够实现高阶函数。高阶函数的定义:
|
||||
|
||||
- 接受一个或多个函数作为输入
|
||||
- 输出一个函数
|
||||
|
||||
因为在 JS 中,函数的本质就是对象,因此可以像其他普通对象一样,作为参数或者返回值进行传递。这也是 JS 中所说的函数是一等公民这个说法的由来。
|
||||
|
||||
## 两者之间本质区别
|
||||
|
||||
介绍完了简单值和复杂值之后,接下来我们从内存存储的角度,来看一下这两种本质上的区别。
|
||||
|
||||
我们知道,内存的存储区域可以分为 **栈** 和 **堆** 这两大块。
|
||||
|
||||
- 栈内存:**栈内存因为其数据大小和生命周期的可预测性而易于管理和快速访问**。栈支持快速的数据分配和销毁过程,**但它不适合复杂的或大规模的数据结构**。
|
||||
- 堆内存:**堆内存更加灵活,可以动态地分配和释放空间,适合存储生命周期长或大小不确定的数据**。使用堆内存可以有效地管理大量的数据,但相对于栈来说,其管理成本更高,访问速度也较慢。
|
||||
|
||||
对于**简单值而言,它们通常存储在栈内存里面**。上面说了,栈内存的特点是管理简单且访问速度快,适用于存储 **大小固定、生命周期短** 的数据。简单值的存储通常包括直接在栈内存中分配的数据空间,并且直接存储了数据的实际值。
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-04-30-031914.png" alt="image-20240430111914109" style="zoom:50%;" />
|
||||
|
||||
而对于复杂值而言,具体的值是存储在 **堆内存** 里面的。因为**复杂值往往大小是不固定的,无法在栈区分配一个固定大小的内存**,因此**具体的数据放在堆里面**。那么这就没有栈区什么事儿了么?倒也不是,**栈区会存储一个内存地址**,通过该内存地址可以访问到堆区里面具体的数据。
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-04-30-032917.png" alt="image-20240430112917412" style="zoom:50%;" />
|
||||
|
||||
另外讲到这里,还有一个非常重要的点要提一下,那就是 **JS 中在调用函数的时候,通通都是值传递,而非引用传递**。
|
||||
|
||||
```js
|
||||
function test(obj) {
|
||||
obj.a = 1000;
|
||||
}
|
||||
const obj = {};
|
||||
console.log(obj); // {}
|
||||
test(obj);
|
||||
console.log(obj); // { a: 1000 }
|
||||
```
|
||||
|
||||
上面的代码,有一定的迷惑性。你看到上面的代码,觉得调用函数之后,obj 发生了真实的修改,所以这是一个引用传递。
|
||||
|
||||
但是这里**仍然是一个值传递**。只不过这个值的背后对应的是一个地址值,这个地址值和简单值一模一样,会被复制一份传递给函数,然后函数内部拿到的是地址值,就可以通过这个地址值找到同一份堆区数据。
|
||||
|
||||
```js
|
||||
function test(obj) {
|
||||
obj = {b:1}; // 这里就赋值了一个新对象,不再使用原来的对象
|
||||
obj.a = 1000;
|
||||
}
|
||||
const obj = {};
|
||||
console.log(obj); // {}
|
||||
test(obj);
|
||||
console.log(obj); // {}
|
||||
```
|
||||
|
||||
如果是真正的引用传递,那么函数内部的 obj 和外部的 obj 是绑在一起的,函数内部对 obj 做任何修改,都会影响外部。但是上面的代码中,很明显在函数内部对 obj 重新赋值后,断开了内外的联系,因此**在 JS 中只有值传递**。
|
||||
|
||||
## 两者之间行为区别
|
||||
|
||||
聊完了本质区别后,接下来我们再来聊一下两者之间行为的区别,主要就下面这么几个点:
|
||||
|
||||
1. 访问方式
|
||||
2. 比较方式
|
||||
3. 动态属性
|
||||
4. 变量赋值
|
||||
|
||||
### 1. 访问方式
|
||||
|
||||
简单值是 **按值访问**,也就是说,一个变量如果存储的是一个简单值,当访问这个变量的时候,得到就是对应的值。
|
||||
|
||||
```js
|
||||
const str = "Hello";
|
||||
console.log(str);
|
||||
```
|
||||
|
||||
复杂值是虽然也是 **按值访问** ,但是由于值对应的是一个 **内存地址值**,一般不能够直接使用,还需要进一步获取地址值背后对应的值。
|
||||
|
||||
```js
|
||||
const obj = {name: "张三"};
|
||||
console.log(obj.name);
|
||||
```
|
||||
|
||||
### 2. 比较方式
|
||||
|
||||
这个比较重要,**无论是简单值也好,复杂值也好,都是进行的值比较**。不过由于复杂值对应的值是一个 **内存地址值**,因此只有在这个内存地址值相同时,才会被认为是相等。
|
||||
|
||||
```js
|
||||
const a = {}; // 内存地址不一样,假设 0x0012ff7c
|
||||
const b = {}; // 内存地址不一样,假设 0x0012ff7d
|
||||
console.log(a === b); // false
|
||||
```
|
||||
|
||||
### 3. 动态属性
|
||||
|
||||
对于**复杂值来讲,可以动态的为其添加属性和方法,这一点简单值是做不到的**。
|
||||
|
||||
如果为简单值动态添加属性,不会报错,会静默失败,访问时返回的值为 undefined
|
||||
|
||||
但如果为简单值动态添加方法,则会报错 xxx is not a function.
|
||||
|
||||
```js
|
||||
const a = 1;
|
||||
a.b = 2;
|
||||
console.log(a.b); // undefined
|
||||
a.c = function(){}
|
||||
a.c(); // error
|
||||
```
|
||||
|
||||
### 4. 变量赋值
|
||||
|
||||
最后说一下关于赋值,记住,它们都是 **将值复制一份** 然后赋值给另外一个变量。
|
||||
|
||||
不过由于复杂值复制的是 **内存地址**,因此修改新的变量会对旧的变量有影响。
|
||||
|
||||
```js
|
||||
let a = 5;
|
||||
let b = a;
|
||||
b = 10; // 不影响 a
|
||||
console.log(a);
|
||||
console.log(b);
|
||||
let obj = {};
|
||||
let obj2 = obj;
|
||||
obj2.name = "张三"; // 会影响 obj
|
||||
console.log(obj); // { name: '张三' }
|
||||
console.log(obj2); // { name: '张三' }
|
||||
obj2 = { name: '张三' };
|
||||
obj2.age = 18; // 不会影响 obj
|
||||
console.log(obj)
|
||||
console.log(obj2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
-EOF-
|
||||
57
02. JS中的数据类型/课堂代码/demo/index.js
Normal file
57
02. JS中的数据类型/课堂代码/demo/index.js
Normal file
@ -0,0 +1,57 @@
|
||||
// console.log(typeof null); // object
|
||||
// console.log(typeof undefined); // undefined
|
||||
|
||||
// console.log(null + 1); // 1
|
||||
// console.log(undefined + 1); // NaN
|
||||
|
||||
// console.log(typeof []); // object
|
||||
// console.log(typeof function () {}); // function
|
||||
// console.log(typeof /abc/); // object
|
||||
|
||||
// function func() {}
|
||||
// // 该函数我是可以正常添加属性和方法的
|
||||
// func.a = 1; // 添加了一个属性
|
||||
// func.test = function () {
|
||||
// console.log("this is a test function");
|
||||
// }; // 添加了一个方法
|
||||
// console.log(func.a); // 1
|
||||
// func.test(); // this is a test function
|
||||
|
||||
// let i = 1;
|
||||
// let obj = {};
|
||||
// obj.a = 1000;
|
||||
// obj.b = [];
|
||||
|
||||
// function test(obj) {
|
||||
// obj = {}; // 这里就赋值了一个新对象,不再使用原来的对象
|
||||
// obj.a = 1000;
|
||||
// }
|
||||
// const obj = {};
|
||||
// console.log(obj); // {}
|
||||
// test(obj);
|
||||
// console.log(obj); // {}
|
||||
|
||||
// const a = {}; // 内存地址不一样,假设 0x0012ff7c
|
||||
// const b = {}; // 内存地址不一样,假设 0x0012ff7d
|
||||
// console.log(a === b); // false
|
||||
|
||||
// const a = 1;
|
||||
// a.b = 2;
|
||||
// console.log(a.b); // undefined
|
||||
// a.c = function(){}
|
||||
// a.c(); // error
|
||||
|
||||
let a = 5;
|
||||
let b = a;
|
||||
b = 10; // 不影响 a
|
||||
console.log(a);
|
||||
console.log(b);
|
||||
let obj = {};
|
||||
let obj2 = obj;
|
||||
obj2.name = "张三"; // 会影响 obj
|
||||
console.log(obj); // { name: '张三' }
|
||||
console.log(obj2); // { name: '张三' }
|
||||
obj2 = { name: '张三' };
|
||||
obj2.age = 18; // 不会影响 obj
|
||||
console.log(obj)
|
||||
console.log(obj2)
|
||||
@ -1,36 +0,0 @@
|
||||
// function a(){
|
||||
// function b(){
|
||||
// function c(){
|
||||
|
||||
// }
|
||||
// c();
|
||||
// }
|
||||
// b();
|
||||
// }
|
||||
// a();
|
||||
|
||||
// var obj = {
|
||||
// name : "xiejie",
|
||||
// age : 18,
|
||||
// score : 100,
|
||||
// newStu : {
|
||||
// name : "zhangsan",
|
||||
// age : 19
|
||||
// }
|
||||
// }
|
||||
|
||||
// var a = 10;
|
||||
// var b = 10;
|
||||
// console.log(a === b); // true
|
||||
|
||||
// var arr1 = [1,2,3];
|
||||
// var arr2 = [1,2,3];
|
||||
// console.log(arr1 === arr2); // false
|
||||
|
||||
var obj = {};
|
||||
obj.name = "xiejie";
|
||||
console.log(obj.name); // xiejie
|
||||
|
||||
var str = "Hello";
|
||||
str.name = "xiejie";
|
||||
console.log(str.name); // undefinded
|
||||
320
02. 值和引用/值和引用.md
320
02. 值和引用/值和引用.md
@ -1,320 +0,0 @@
|
||||
# 值和引用
|
||||
|
||||
|
||||
|
||||
## 经典真题
|
||||
|
||||
|
||||
|
||||
- *JS* 的基本数据类型有哪些?基本数据类型和引用数据类型的区别
|
||||
|
||||
|
||||
|
||||
## 值和引用相关内容
|
||||
|
||||
|
||||
|
||||
在 *JavaScript* 中,数据类型整体上来讲可以分为两大类:**基本类型**和**引用数据类型**
|
||||
|
||||
基本数据类型,一共有 *6* 种:
|
||||
|
||||
```text
|
||||
string,symbol,number,boolean,undefined,null
|
||||
```
|
||||
|
||||
其中 *symbol* 类型是在 *ES6* 里面新添加的基本数据类型。
|
||||
|
||||
引用数据类型,就只有 *1* 种:
|
||||
|
||||
```js
|
||||
object
|
||||
```
|
||||
|
||||
基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。
|
||||
|
||||
|
||||
|
||||
那么两者之间具体有什么区别呢?我们一点一点来看:
|
||||
|
||||
|
||||
|
||||
#### 1. 简单值(原始值)
|
||||
|
||||
**简单值是表示 *JavaScript* 中可用的数据或信息的最底层形式或最简单形式。**简单类型的值被称为简单值,是因为它们是**不可细化**的。
|
||||
|
||||
也就是说,数字是数字,字符串是字符串,布尔值是 *true* 或 *false*,*null* 和 *undefined* 就是 *null* 和 *undefined*。这些值本身很简单,不能够再进行拆分。
|
||||
|
||||
由于简单值的数据大小是固定的,所以**简单值的数据是存储于内存中的栈区里面的。**
|
||||
|
||||
|
||||
|
||||
要简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
下面是具体的代码示例:
|
||||
|
||||
```js
|
||||
var str = "Hello World";
|
||||
var num = 10;
|
||||
var bol = true;
|
||||
var myNull = null;
|
||||
var undef = undefined;
|
||||
console.log(typeof str); // string
|
||||
console.log(typeof num); // number
|
||||
console.log(typeof bol); // boolean
|
||||
console.log(typeof myNull); // object
|
||||
console.log(typeof undef); // undefined
|
||||
```
|
||||
|
||||
这里面 *null* 比较特殊,打印出来是 *object*,这是由于历史原因所遗留下来的问题。
|
||||
|
||||
是来源于 *JavaScript* 从第一个版本开始时的一个 *bug*,并且这个 *bug* 无法被修复。因为修复会破坏现有的代码。
|
||||
|
||||
具体原因是因为不同的对象在底层都表现为二进制,在 *JavaScript* 中二进制前三位都为 *0* 的话会被判断为 *object* 类型,*null* 的二进制全部为 *0*,自然前三位也是 *0*,所以执行 *typeof* 值会返回 *object*。
|
||||
|
||||
|
||||
|
||||
例外,当我们打印 *null == undefined* 的时候,返回的是 *true*,这也是面试时经常会被问到的一个问题。
|
||||
|
||||
这两个值都表示“无”的意思。
|
||||
|
||||
通常情况下, 当我们试图访问某个不存在的或者没有赋值的变量时,就会得到一个 *undefined* 值。*Javascript* 会自动将声明是没有进行初始化的变量设为 *undifined*。
|
||||
|
||||
而 *null* 值表示空,*null* 不能通过 *Javascript* 来自动赋值,也就是说必须要我们自己手动来给某个变量赋值为 *null*。
|
||||
|
||||
那么为什么 *JavaScript* 要设置两个表示"无"的值呢?
|
||||
|
||||
这其实也是因为历史原因。
|
||||
|
||||
*1995* 年 *JavaScript* 诞生时,最初像 *Java* 一样,只设置了 *null* 作为表示"无"的值。根据 *C* 语言的传统,*null* 被设计成可以自动转为 *0*。
|
||||
|
||||
但是,*JavaScript* 的设计者,觉得这样做还不够,主要有以下两个原因。
|
||||
|
||||
1. *null* 像在 *Java* 里一样,被当成一个对象。但是,*JavaScript* 的数据类型分成原始类型(*primitive*)和复合类型(*complex*)两大类,作者觉得表示“无”的值最好不是对象。
|
||||
2. *JavaScript* 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果 *null* 自动转为 *0*,很不容易发现错误。
|
||||
|
||||
因此,作者又设计了一个 *undefined*。**这里注意:先有 *null* 后有 *undefined* 出来,*undefined* 是为了填补之前的坑。**
|
||||
|
||||
*JavaScript* 的最初版本是这样区分的:
|
||||
|
||||
|
||||
|
||||
*null* 是一个表示“无”的对象(空对象指针),转为数值时为 *0*;
|
||||
|
||||
典型用法是:
|
||||
|
||||
- 作为函数的参数,表示该函数的参数不是对象。
|
||||
|
||||
- 作为对象原型链的终点。
|
||||
|
||||
|
||||
|
||||
*undefined* 是一个表示"无"的原始值,转为数值时为 *NaN*。
|
||||
|
||||
典型用法是:
|
||||
|
||||
- 变量被声明了,但没有赋值时,就等于 *undefined*。
|
||||
- 调用函数时,应该提供的参数没有提供,该参数等于 *undefined*。
|
||||
- 对象没有赋值的属性,该属性的值为 *undefined*。
|
||||
- 函数没有返回值时,默认返回 *undefined*。
|
||||
|
||||
|
||||
|
||||
#### 2. 复杂值(引用值)
|
||||
|
||||
在 *JavaScript* 中,对象就是一个复杂值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。
|
||||
|
||||
**复杂值在内存中的大小是未知的,因为复杂值可以包含任何值,而不是一个特定的已知值,所以复杂值的数据都是存储于堆区里面。**
|
||||
|
||||
如下图所示:
|
||||
|
||||

|
||||
|
||||
下面是具体的代码示例:
|
||||
|
||||
```js
|
||||
// 简单值
|
||||
var a1 = 0;
|
||||
var a2 = "this is str";
|
||||
var a3 = null
|
||||
|
||||
// 复杂值
|
||||
var c = [1, 2, 3];
|
||||
var d = {m: 20};
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3. 访问方式
|
||||
|
||||
**按值访问**
|
||||
|
||||
简单值是作为不可细化的值进行存储和使用的,引用它们会转移其值。
|
||||
|
||||
```js
|
||||
var str = "Hello";
|
||||
var str2 = str;
|
||||
str = null;
|
||||
console.log(str,str2); // null "Hello"
|
||||
```
|
||||
|
||||
**引用访问**
|
||||
|
||||
复杂值是通过引用进行存储和操作的,而不是实际的值。创建一个包含复杂对象的变量时,其值是内存中的一个引用地址。引用一个复杂对象时,使用它的名称(即变量或对象属性)通过内存中的引用地址获取该对象值。
|
||||
|
||||
```js
|
||||
var obj = {};
|
||||
var obj2 = obj;
|
||||
obj.name = "zhangsan";
|
||||
console.log(obj.name); // zhangsan
|
||||
console.log(obj2.name); // zhangsan
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 4. 比较方式
|
||||
|
||||
简单值采用值比较,而复杂值采用引用比较。复杂值只有在引用相同的对象(即有相同的地址)时才相等。即使是包含相同对象的两个变量也彼此不相等,因为它们并不指向同一个对象。
|
||||
|
||||
示例 1:
|
||||
|
||||
```js
|
||||
var a = 10;
|
||||
var b = 10;
|
||||
var c = new Number(10);
|
||||
var d = c;
|
||||
console.log(a === b); // true
|
||||
console.log(a === c); // false
|
||||
console.log(a === c); // false
|
||||
console.log(a == c); // true
|
||||
d = 10;
|
||||
console.log(d == c); // true
|
||||
console.log(d === c); // false
|
||||
```
|
||||
|
||||
示例 2:
|
||||
|
||||
```js
|
||||
var obj = {name : 'zhangsan'};
|
||||
var obj2 = {name : 'zhangsan'};
|
||||
console.log(obj == obj2); // false
|
||||
console.log(obj === obj2); // false
|
||||
var obj3 = {name : 'zhangsan'};
|
||||
var obj4 = obj3;
|
||||
console.log(obj3 == obj4); // true
|
||||
console.log(obj3 === obj4); // ture
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 5. 动态属性
|
||||
|
||||
对于复杂值,可以为其添加属性和方法,也可以改变和删除其属性和方法。但简单值不可以:
|
||||
|
||||
```js
|
||||
var str = 'test';
|
||||
str.abc = true;
|
||||
console.log(str.abc); // undefined
|
||||
var obj = {};
|
||||
obj.abc = true;
|
||||
console.log(obj.abc); // true
|
||||
```
|
||||
|
||||
|
||||
|
||||
复杂值支持动态对象属性,因为我们可以定义对象,然后创建引用,再更新对象,并且所有指向该对象的变量都会获得更新。
|
||||
|
||||
一个新变量指向现有的复杂对象,并没有复制该对象。这就是复杂值有时被称为引用值的原因。复杂值可以根据需求有任意多个引用,即使对象改变,它们也总是指向同一个对象
|
||||
|
||||
```js
|
||||
var obj = {name : 'zhangsan'};
|
||||
var obj2 = obj;
|
||||
var obj3 = obj2;
|
||||
obj.name = 'abc';
|
||||
console.log(obj.name, obj2.name, obj3.name);
|
||||
// abc abc abc
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 6. 变量赋值
|
||||
|
||||
最后说一下关于变量的赋值,其实是可以分为直接赋值和引用赋值的。直接赋值,就是指将简单值赋值给变量,而引用赋值是指将一个复杂值的引用赋值给变量,这个引用指向堆区实际存在的数据。
|
||||
|
||||
**直接赋值**
|
||||
|
||||
```js
|
||||
var a = 3;
|
||||
var b = a;
|
||||
b = 5;
|
||||
console.log(a); // 3
|
||||
```
|
||||
|
||||
**引用赋值**
|
||||
|
||||
```js
|
||||
var a = {value : 1};
|
||||
var b = a;
|
||||
b.value = 10;
|
||||
console.log(a.value); // 10
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 真题解答
|
||||
|
||||
|
||||
|
||||
- *JS* 的基本数据类型有哪些?基本数据类型和引用数据类型的区别
|
||||
|
||||
> 参考答案:
|
||||
>
|
||||
> 在 *JavaScript* 中,数据类型整体上来讲可以分为两大类:**基本类型**和**引用数据类型**
|
||||
>
|
||||
> 基本数据类型,一共有 *6* 种:
|
||||
>
|
||||
> ```text
|
||||
> string,symbol,number,boolean,undefined,null
|
||||
> ```
|
||||
>
|
||||
> 其中 *symbol* 类型是在 *ES6* 里面新添加的基本数据类型。
|
||||
>
|
||||
> 引用数据类型,就只有 *1* 种:
|
||||
>
|
||||
> ```js
|
||||
> object
|
||||
> ```
|
||||
>
|
||||
> 基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。
|
||||
>
|
||||
> 两者的区别在于:
|
||||
>
|
||||
> **原始值是表示 *JavaScript* 中可用的数据或信息的最底层形式或最简单形式。**简单类型的值被称为原始值,是因为它们是**不可细化**的。
|
||||
>
|
||||
> 也就是说,数字是数字,字符是字符,布尔值是 *true* 或 *false*,*null* 和 *undefined* 就是 *null* 和 *undefined*。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以**原始值的数据是存储于内存中的栈区里面的。**
|
||||
>
|
||||
> 在 *JavaScript* 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。**引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。**
|
||||
>
|
||||
> 最后总结一下两者的区别:
|
||||
>
|
||||
> 1. 访问方式
|
||||
> - 原始值:访问到的是值
|
||||
> - 引用值:访问到的是引用地址
|
||||
> 2. 比较方式
|
||||
> - 原始值:比较的是值
|
||||
> - 引用值:比较的是地址
|
||||
>
|
||||
> 3. 动态属性
|
||||
> - 原始值:无法添加动态属性
|
||||
> - 引用值:可以添加动态属性
|
||||
> 4. 变量赋值
|
||||
> - 原始值:赋值的是值
|
||||
> - 引用值:赋值的是地址
|
||||
|
||||
|
||||
|
||||
-*EOF*-
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
// var person = {
|
||||
// arms : 2,
|
||||
// legs : 2
|
||||
// }
|
||||
// Object.create 方法第一个参数是原型对象
|
||||
// Object.create 方法接收第二个参数:对象
|
||||
// 该对象里面可以设置多个键值对
|
||||
// 每个键就是新对象的属性,所对应的值是一个属性描述符
|
||||
// var zhangsan = Object.create(person,{
|
||||
// name : {
|
||||
// value : "zhangsan",
|
||||
// enumerable : true
|
||||
// },
|
||||
// age : {
|
||||
// value : 18,
|
||||
// enumerable : true
|
||||
// }
|
||||
// });
|
||||
// person 实际上就是 zhangsan 这个对象的原型对象
|
||||
// console.log(zhangsan);
|
||||
// console.log(zhangsan.arms);
|
||||
// console.log(zhangsan.legs);
|
||||
|
||||
// console.log(zhangsan.__proto__ === person);
|
||||
|
||||
// var zhangxiaosan = Object.create(zhangsan, {
|
||||
// name : {
|
||||
// value : "zhangxiaosan",
|
||||
// enumerable : true
|
||||
// },
|
||||
// born : {
|
||||
// value : "beijing",
|
||||
// enumerable: true
|
||||
// }
|
||||
// })
|
||||
// console.log(zhangxiaosan.name); // zhangxiaosan
|
||||
// console.log(zhangxiaosan.arms); // 2
|
||||
// console.log(zhangxiaosan.gender); // undefined
|
||||
|
||||
// 总结,当查找一个对象的属性的时候,如果该对象上面没有这个属性,
|
||||
// 则会去该对象上面的原型对象上面进行查找
|
||||
|
||||
// console.log(zhangxiaosan.__proto__ === zhangsan);
|
||||
// console.log(zhangxiaosan.__proto__.__proto__ === person);
|
||||
|
||||
// 之后,随着 js 语言的发展,我们还是希望 js 能够像标准的面向对象语言一样
|
||||
// 通过类来批量的生产对象
|
||||
// 早期 js 通过构造函数来模拟其他语言里面的类
|
||||
|
||||
function Computer(name, price){
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
// 将方法挂在原型对象上面
|
||||
Computer.prototype.showPrice = function(){
|
||||
console.log(`${this.name}的电脑价格为${this.price}`);
|
||||
}
|
||||
var apple = new Computer("苹果", 15000);
|
||||
// console.log(apple);
|
||||
apple.showPrice();
|
||||
|
||||
var huawei = new Computer("华为", 12000);
|
||||
// console.log(huawei);
|
||||
huawei.showPrice();
|
||||
|
||||
// 虽然上面的方式模拟出了其他语言中面向对象的语言创建对象的方式
|
||||
// 但是在 js 底层还是基于原型来创建的对象
|
||||
|
||||
// 比如我们的对象除了有属性,一般还有方法
|
||||
// 方法一般会选择挂到原型对象上面
|
||||
|
||||
// console.log(apple.__proto__ === Computer.prototype);
|
||||
// console.log(apple.constructor === Computer);
|
||||
|
||||
// 内置的构造函数也有这样的三角关系
|
||||
// var arr = [];
|
||||
// console.log(Array.prototype === arr.__proto__);
|
||||
// console.log(Array.__proto__ === Computer.__proto__);
|
||||
// console.log(Date.__proto__ === Computer.__proto__);
|
||||
// console.log(String.__proto__ === Computer.__proto__);
|
||||
// console.log(Number.__proto__ === Computer.__proto__);
|
||||
// console.log(Boolean.__proto__ === Computer.__proto__);
|
||||
// console.log(Computer.__proto__);
|
||||
|
||||
// 验证原型对象的终点是 null
|
||||
// console.log(apple.__proto__.__proto__.__proto__); // null
|
||||
// console.log(apple.__proto__.__proto__ === Object.prototype);
|
||||
// console.log(Object.prototype.__proto__); // null
|
||||
|
||||
// console.log(Computer.__proto__.__proto__ === Object.prototype); // true
|
||||
|
||||
// Object.prototype 再往上一层(__proto__)就是 null
|
||||
// console.log(Object.prototype.constructor);
|
||||
|
||||
// console.log(Computer.__proto__ === Object.prototype.constructor.__proto__);
|
||||
|
||||
console.log(Computer.__proto__.__proto__.__proto__); // null
|
||||
console.log(Computer.__proto__.constructor.__proto__ === Computer.__proto__); // true
|
||||
console.log(Computer.__proto__.__proto__.constructor.__proto__ === Computer.__proto__);
|
||||
@ -1,368 +0,0 @@
|
||||
# 原型和原型链
|
||||
|
||||
|
||||
|
||||
## 经典真题
|
||||
|
||||
|
||||
|
||||
- 说一说你对 *JavaScript* 中原型与原型链的理解?(美团 *2019*年)
|
||||
- 对一个构造函数实例化后,它的原型链指向什么?
|
||||
|
||||
|
||||
|
||||
## 原型与原型链介绍
|
||||
|
||||
|
||||
|
||||
在 *Brendan Eich* 设计 *JavaScript* 时,借鉴了 *Self* 和 *Smalltalk* 这两门基于原型的语言。
|
||||
|
||||
|
||||
|
||||
之所以选择基于原型的对象系统,是因为 *Brendan Eich* 一开始就没有打算在 *JavaScript* 中加入类的概念,因为 *JavaScript* 的设计初衷就是为非专业的开发人员(例如网页设计者)提供一个方便的工具。由于大部分网页设计者都没有任何的编程背景,所以在设计 *JavaScript* 时也是尽可能使其简单、易学。
|
||||
|
||||
|
||||
|
||||
这因为如此,*JavaScript* 中的原型以及原型链成为了这门语言最大的一个特点,在面试的时候,面试官也经常会围绕原型和原型链展开提问。
|
||||
|
||||
|
||||
|
||||
*JavaScript* 是一门基于原型的语言,**对象的产生是通过原型对象而来的**。
|
||||
|
||||
|
||||
|
||||
*ES5* 中提供了 *Object.create* 方法,可以用来克隆对象。
|
||||
|
||||
|
||||
|
||||
示例如下:
|
||||
|
||||
```js
|
||||
const person = {
|
||||
arms: 2,
|
||||
legs: 2,
|
||||
walk() {
|
||||
console.log('walking');
|
||||
}
|
||||
}
|
||||
const zhangsan = Object.create(person);
|
||||
console.log(zhangsan.arms); // 2
|
||||
console.log(zhangsan.legs); // 2
|
||||
zhangsan.walk(); // walking
|
||||
console.log(zhangsan.__proto__ === person); // true
|
||||
```
|
||||
|
||||
|
||||
|
||||
在上面的示例中,我们通过 *Object.create* 方法来对 *person* 对象进行克隆,克隆出来了一个名为 *zhangsan* 的对象,所以 *person* 对象就是 *zhangsan* 这个对象的原型对象。
|
||||
|
||||
|
||||
|
||||
*person* 对象上面的属性和方法,*zhangsan* 这个对象上面都有。
|
||||
|
||||
|
||||
|
||||
通过 \__*proto*__ 属性,我们可以访问到一个对象的原型对象。
|
||||
|
||||
|
||||
|
||||
从上面的代码可以看出,当我们打印`zhangsan.__proto__ === person`,返回的是 *true* ,因为对于 *zhangsan* 这个对象而言,它的原型对象就是 *person* 这个对象。
|
||||
|
||||
|
||||
|
||||
我们在使用 *Object.create* 方法来克隆对象的时候,还可以传入第 *2* 个参数,第 *2* 个参数是一个 *JSON* 对象,该对象可以书写新对象的**新属性**以及**属性特性**。
|
||||
|
||||
|
||||
|
||||
通过这种方式,基于对象创建的新对象,可以继承祖辈对象的属性和方法,这其实就是一个继承的关系,来看一个示例:
|
||||
|
||||
|
||||
|
||||
```js
|
||||
const person = {
|
||||
arms: 2,
|
||||
legs: 2,
|
||||
walk() {
|
||||
console.log('walking');
|
||||
}
|
||||
}
|
||||
const zhangsan = Object.create(person, {
|
||||
name: {
|
||||
value: "zhangsan",
|
||||
},
|
||||
age: {
|
||||
value: 18,
|
||||
},
|
||||
born: {
|
||||
value: "chengdu"
|
||||
}
|
||||
});
|
||||
const zhangxiaosan = Object.create(zhangsan, {
|
||||
name: {
|
||||
value: "zhangxiaosan"
|
||||
},
|
||||
age: {
|
||||
value: 1
|
||||
}
|
||||
})
|
||||
console.log(zhangxiaosan.name); // zhangxiaosan
|
||||
console.log(zhangxiaosan.age); // 1
|
||||
console.log(zhangxiaosan.born); // chengdu
|
||||
console.log(zhangxiaosan.arms); // 2
|
||||
console.log(zhangxiaosan.legs); // 2
|
||||
zhangxiaosan.walk(); // walking
|
||||
console.log(zhangsan.isPrototypeOf(zhangxiaosan)); // true
|
||||
console.log(person.isPrototypeOf(zhangxiaosan)); // true
|
||||
```
|
||||
|
||||
|
||||
|
||||
该例中,*zhangsan* 这个对象是从 *person* 这个对象克隆而来的,而 *zhangxiaosan* 这个对象又是从 *zhangsan* 这个对象克隆而来,以此**形成了一条原型链**。无论是 *person* 对象,还是 *zhangsan* 对象上面的属性和方法,*zhangxiaosan* 这个对象都能继承到。
|
||||
|
||||
|
||||
|
||||
来看下面的图:
|
||||
|
||||
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-08-10-050603.png" alt="image-20210810130602385" style="zoom:50%;" />
|
||||
|
||||
|
||||
|
||||
这就是 *JavaScript* 中最原始的创建对象的方式,一个对象是通过克隆另外一个对象所得到的。就像克隆羊多莉一样,通过克隆可以创造一个一模一样的对象,被克隆的对象是新对象的原型对象。
|
||||
|
||||
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-08-10-051614.png" alt="image-20210810131613519" style="zoom: 33%;" />
|
||||
|
||||
|
||||
|
||||
但是,随着 *JavaScript* 语言的发展,这样创建对象的方式还是太过于麻烦了。开发者还是期望 *JavaScript* 能够像 *Java、C#* 等标准面向对象语言一样,通过类来批量的生成对象。于是出现了通过构造函数来模拟类的形式。
|
||||
|
||||
|
||||
|
||||
来看下面的例子:
|
||||
|
||||
|
||||
|
||||
```js
|
||||
function Computer(name, price) {
|
||||
// 属性写在类里面
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
// 方法挂在原型对象上面
|
||||
Computer.prototype.showSth = function () {
|
||||
console.log(`这是一台${this.name}电脑`);
|
||||
}
|
||||
|
||||
const apple = new Computer("苹果", 12000);
|
||||
console.log(apple.name); // 苹果
|
||||
console.log(apple.price); // 12000
|
||||
apple.showSth(); // 这是一台苹果电脑
|
||||
|
||||
const huawei = new Computer("华为", 7000);
|
||||
console.log(huawei.name); // 华为
|
||||
console.log(huawei.price); // 7000
|
||||
huawei.showSth(); // 这是一台华为电脑
|
||||
```
|
||||
|
||||
|
||||
|
||||
在上面的例子中,我们书写了一个 *Computer* 的函数,我们称之为构造函数,为了区分普通函数和构造函数,一般构造函数的函数名**首字母会大写**。
|
||||
|
||||
|
||||
|
||||
区别于普通函数的直接调用,构造函数一般通过配合 *new* 关键字一起使用,每当我们 *new* 一次,就会生成一个新的对象,而在构造函数中的 *this* 就指向这个新生成的对象。
|
||||
|
||||
|
||||
|
||||
在上面的例子中,我们 *new* 了两次,所以生成了两个对象,我们把这两个对象分别存储到 *apple* 和 *huawei* 这两个变量里面。
|
||||
|
||||
|
||||
|
||||
有一个非常有意思的现象,就是我们在书写 *Computer* 构造函数的实例方法的时候,并没有将这个方法书写在构造函数里面,而是写在了 *Computer.prototype* 上面,那么这个 *Computer.prototype* 是啥呢?
|
||||
|
||||
|
||||
|
||||
这个 *Computer.prototype* 实际上就是 *Computer* 实例对象的原型对象。要搞清楚这个,看下面的图:
|
||||
|
||||
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-10-27-063331.png" alt="image-20211027143330933" style="zoom:50%;" />
|
||||
|
||||
|
||||
|
||||
这是最重要的一个三角关系,也是我往往要求学生记下来的三角关系。
|
||||
|
||||
|
||||
|
||||
通过上图,我们可以得出以下的结论:
|
||||
|
||||
|
||||
|
||||
- *JavaScript* 中每个对象都有一个原型对象。可以通过 \__*proto*__ 属性来访问到对象的原型对象。
|
||||
- 构造函数的 *prototype* 属性指向一个对象,这个对象是该构造函数实例化出来的对象的原型对象。
|
||||
- 原型对象的 *constructor* 属性也指向其构造函数。
|
||||
- 实例对象的 *constructor* 属性是从它的原型对象上面访问到。
|
||||
|
||||
|
||||
|
||||
实践才是检验真理的唯一标准。接下来我们在代码中来验证一下:
|
||||
|
||||
|
||||
|
||||
```js
|
||||
function Computer(name, price) {
|
||||
// 属性写在类里面
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
// 方法挂在原型对象上面
|
||||
Computer.prototype.showSth = function () {
|
||||
console.log(`这是一台${this.name}电脑`);
|
||||
}
|
||||
|
||||
const apple = new Computer("苹果", 12000);
|
||||
|
||||
console.log(apple.__proto__ === Computer.prototype); // true
|
||||
console.log(apple.__proto__.constructor === Computer); // true
|
||||
```
|
||||
|
||||
|
||||
|
||||
在上面的代码中,*apple* 是从 *Computer* 这个构造函数中实例化出来的对象,我们通过 \__*proto*__ 来访问到 *apple* 的原型对象,而这个原型对象和 *Computer.prototype* 是等价的。另外, 我们也发现 *apple* 和它原型对象的 *constructor* 属性都指向 *Computer* 这个构造函数。
|
||||
|
||||
|
||||
|
||||
接下来我们还可以来验证内置的构造函数是不是也是这样的关系,如下:
|
||||
|
||||
```js
|
||||
function Computer(name, price) {
|
||||
// 属性写在类里面
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
// 方法挂在原型对象上面
|
||||
Computer.prototype.showSth = function () {
|
||||
console.log(`这是一台${this.name}电脑`);
|
||||
}
|
||||
|
||||
const apple = new Computer("苹果", 12000);
|
||||
|
||||
console.log(apple.__proto__ === Computer.prototype); // true
|
||||
console.log(apple.__proto__.constructor === Computer); // true
|
||||
|
||||
// 数组的三角关系
|
||||
var arr = [];
|
||||
console.log(arr.__proto__ === Array.prototype); // true
|
||||
|
||||
// 其实所有的构造函数的原型对象都相同
|
||||
console.log(Computer.__proto__ === Array.__proto__); // true
|
||||
console.log(Computer.__proto__ === Date.__proto__); // true
|
||||
console.log(Computer.__proto__ === Number.__proto__); // true
|
||||
console.log(Computer.__proto__ === Function.__proto__); // true
|
||||
console.log(Computer.__proto__ === Object.__proto__); // true
|
||||
console.log(Computer.__proto__); // {}
|
||||
```
|
||||
|
||||
通过上面的代码,我们发现所有的构造函数,无论是自定义的还是内置的,它们的原型对象都是同一个对象。
|
||||
|
||||
|
||||
|
||||
如果你能够把上面的三角关系理清楚,恭喜你,你已经把整个原型和原型链的知识掌握一大部分。
|
||||
|
||||
|
||||
|
||||
如果你还想继续往下深究,那么上面的图可以扩展成这样:
|
||||
|
||||
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-10-27-064429.png" alt="image-20211027144428458" style="zoom:50%;" />
|
||||
|
||||
|
||||
|
||||
在 *JavaScript* 中,每一个对象,都有一个原型对象。而原型对象上面也有一个自己的原型对象,一层一层向上找,最终会到达 *null*。
|
||||
|
||||
|
||||
|
||||
我们可以在上面代码的基础上,继续进行验证,如下:
|
||||
|
||||
|
||||
|
||||
```js
|
||||
function Computer(name, price) {
|
||||
// 属性写在类里面
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
// 方法挂在原型对象上面
|
||||
Computer.prototype.showSth = function () {
|
||||
console.log(`这是一台${this.name}电脑`);
|
||||
}
|
||||
|
||||
var apple = new Computer("苹果", 12000);
|
||||
|
||||
console.log(apple.__proto__.__proto__); // [Object: null prototype] {}
|
||||
console.log(apple.__proto__.__proto__.__proto__); // null
|
||||
console.log(apple.__proto__.__proto__ === Object.prototype); // true
|
||||
```
|
||||
|
||||
|
||||
|
||||
可以看到,在上面的代码中,我们顺着原型链一层一层往上找,最终到达了 *null*。
|
||||
|
||||
但是目前来看我们这个图还是不完整,既然构造函数的原型对象也是对象,那么必然该对象也有自己的原型,所以完整的图其实如下:
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-10-27-072845.png" alt="image-20211027152845110" style="zoom:50%;" />
|
||||
|
||||
下面可以简单验证一下,如下:
|
||||
|
||||
```js
|
||||
// 自定义构造函数函数
|
||||
function Computer() {}
|
||||
|
||||
console.log(Computer.__proto__.__proto__.__proto__); // null
|
||||
console.log(Computer.__proto__.constructor.__proto__ === Computer.__proto__); // true
|
||||
console.log(Computer.__proto__.__proto__.constructor.__proto__ === Computer.__proto__); // true
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 真题解答
|
||||
|
||||
|
||||
|
||||
- 说一说你对 *JavaScript* 中原型与原型链的理解?(美团 *2019*年)
|
||||
|
||||
> 参考答案:
|
||||
>
|
||||
> - 每个对象都有一个 \__*proto*__ 属性,该属性指向自己的原型对象
|
||||
> - 每个构造函数都有一个 *prototype* 属性,该属性指向实例对象的原型对象
|
||||
> - 原型对象里的 *constructor* 指向构造函数本身
|
||||
>
|
||||
> 如下图:
|
||||
>
|
||||
> <img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-10-27-063331.png" alt="image-20211027143330933" style="zoom:50%;" />
|
||||
>
|
||||
> 每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。
|
||||
>
|
||||
> 当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
|
||||
|
||||
|
||||
|
||||
- 对一个构造函数实例化后,它的原型链指向什么?
|
||||
|
||||
> 参考答案:
|
||||
>
|
||||
> 指向该构造函数实例化出来对象的原型对象。
|
||||
>
|
||||
> 对于构造函数来讲,可以通过 *prototype* 访问到该对象。
|
||||
>
|
||||
> 对于实例对象来讲,可以通过隐式属性 \__*proto*__ 来访问到。
|
||||
|
||||
|
||||
|
||||
-*EOF*-
|
||||
|
||||
380
06. 原型链/课件资料/原型链.md
Normal file
380
06. 原型链/课件资料/原型链.md
Normal file
@ -0,0 +1,380 @@
|
||||
# 原型链
|
||||
|
||||
> 面试题:
|
||||
>
|
||||
> 说一说你对 JS 中原型与原型链的理解?(美团 2019 年)
|
||||
>
|
||||
> 对一个构造函数实例化后,它的原型链指向什么?
|
||||
|
||||
- 生产对象的方式
|
||||
- 原型对象与原型链
|
||||
- 原型链相关方法
|
||||
|
||||
## 生产对象的方式
|
||||
|
||||
不同的语言,**生产对象的方式**其实并不相同,整体来讲,**可以分为两大类**:
|
||||
|
||||
1. 基于**类**生产对象
|
||||
2. 基于**原型**生产对象
|
||||
|
||||
### 1. 基于类生产对象
|
||||
|
||||
这种生产对象的方式可能是最常见的方式,很多语言中**要生产一个对象,都需要先书写一个类,然后通过类来实例化对象**。
|
||||
|
||||
Java
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public void sayHello() {
|
||||
System.out.println("我的名字是" + name + ",我今年" + age + "岁");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Person p = new Person("张三", 18);
|
||||
p.sayHello();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Python
|
||||
|
||||
```py
|
||||
class Person:
|
||||
def __init__(self, name, age):
|
||||
self.name = name
|
||||
self.age = age
|
||||
|
||||
def say_hello(self):
|
||||
print(f"我的名字是{self.name},我今年{self.age}岁")
|
||||
|
||||
p = Person("张三", 18)
|
||||
p.say_hello()
|
||||
```
|
||||
|
||||
PHP
|
||||
|
||||
```php
|
||||
class Person {
|
||||
private $name;
|
||||
private $age;
|
||||
|
||||
public function __construct($name, $age) {
|
||||
$this->name = $name;
|
||||
$this->age = $age;
|
||||
}
|
||||
|
||||
public function sayHello() {
|
||||
echo "我的名字是" . $this->name . ",我今年" . $this->age . "岁\n";
|
||||
}
|
||||
}
|
||||
|
||||
$p = new Person("张三", 18);
|
||||
$p->sayHello();
|
||||
```
|
||||
|
||||
可以看到,很多主流的编程语言,都是通过实例化类的方式来产生对象。
|
||||
|
||||
但是,这并非唯一的方式。
|
||||
|
||||
### 2. 基于原型生产对象
|
||||
|
||||
还有一种方式,则是基于原型来生产对象。
|
||||
|
||||
这种方式的核心思想就是:**先有一个对象 A,然后你要生产一个新的对象 B,就先克隆一份对象 A 从而得到新对象 B,新的对象 B 可以添加新的属性或者方法,对于对象 B 而言,对象 A 就是自己的原型对象**。
|
||||
|
||||

|
||||
|
||||
采用这种生产对象方式的语言,虽然不像上面所罗列的那些语言那么主流,但是也确确实实存在:
|
||||
|
||||
1. **Self** - Self语言彻底采用原型模式,没有类的概念,对象直接从其他对象克隆并可能修改。
|
||||
2. **Lua** - 虽然Lua中有模块和包的概念,但其表(table)结构可以用来实现原型式继承。
|
||||
3. **Io** - Io是一个纯原型语言,所有对象都来自于克隆其他对象。
|
||||
4. **Proto** - 一个较少知的语言,专门设计为原型继承语言。
|
||||
|
||||
### 3. JS 生产对象
|
||||
|
||||
布兰登・艾奇在设计这门语言时,**选择了原型的方式来生产对象**,他给出的理由有两个:
|
||||
|
||||
- 他自己本身是一个 Lisp 程序员,主要方向和兴趣是函数式编程,因此在编程范式上更喜欢属于声明式编程的函数式编程。
|
||||
- JS 设计的初衷,定位是一门面向非专业的开发人员(例如网页设计者)的语言。由于大部分网页设计者都没有任何的编程背景,因此这门语言应该尽可能的简单。
|
||||
|
||||
在 JS 中,你可以很轻松的查看一个对象的原型。
|
||||
|
||||
```js
|
||||
const obj = {};
|
||||
console.log(obj.__proto__);
|
||||
```
|
||||
|
||||
通过上面的例子,我们可以得出一个结论:**在 JS 中,无论你这个对象是如何书写的,该对象都有自己的原型对象**。
|
||||
|
||||
在 ES5 中提供了一个 Object.create 方法,该方法的第二个参数就可以指定对象的原型对象。
|
||||
|
||||
```js
|
||||
const person = {
|
||||
arm: 2,
|
||||
legs: 2,
|
||||
walk() {
|
||||
console.log("walking");
|
||||
},
|
||||
};
|
||||
|
||||
const john = Object.create(person, {
|
||||
name: {
|
||||
value: "John",
|
||||
enumerable: true,
|
||||
},
|
||||
age: {
|
||||
value: 18,
|
||||
enumerable: true,
|
||||
},
|
||||
});
|
||||
console.log(john.__proto__ === person); // true
|
||||
```
|
||||
|
||||
布兰登・艾奇最初在设计这门语言时的构想是很美好的,但是现实是很残酷的。
|
||||
|
||||
当时的大环境下,流行的是“基于类生产对象”的方式,其中又以 Java、C++ 这样的语言最为代表。另外,当时网景公司的整个管理层,都是 Java 语言的信徒,因此在 1995 年 5 月,网景公司做出决策,未来的网页脚本语言必须“看上去与 Java 足够相似”,但是需要比 Java 简单。
|
||||
|
||||
没办法,受到了公司高层的命令,布兰登・艾奇游不得不对 JS 进行改造,添加了 this、new 这些关键字,使其**看上去像是基于类生产的对象**。不过早期没有 class 关键字,怎么办呢?没错,就是**使用 function 来模拟类,为了区分普通函数,一个不成文的规定就是构造函数名称首字母大写。**
|
||||
|
||||
```js
|
||||
function Computer(name, price){
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
```
|
||||
|
||||
这里的构造函数本身是普通的函数,但如果你使用 new 的方式来调用,执行机制则和普通的函数调用不一样,会经历如下的步骤:
|
||||
|
||||
1. 创建一个空的简单 JS 对象(即 { } )
|
||||
2. 为步骤 1 新创建的对象添加属性 \__proto__,将该属性链接至构造函数的原型对象
|
||||
3. 将步骤 1 新创建的对象作为 this 的上下文
|
||||
4. 如果该函数没有返回对象,则返回 this
|
||||
|
||||
```js
|
||||
function Computer(name, price) {
|
||||
// 1. 创建一个普通的对象
|
||||
// const obj = {};
|
||||
|
||||
// 2. 设置该对象的原型对象
|
||||
// obj.__proto__ = Computer.prototype;
|
||||
|
||||
// 3. 设置 this 的指向,指向该 obj
|
||||
// this ---> obj
|
||||
this.name = name; // {name: "华为"}
|
||||
this.price = price; // {name: "华为", price: 5000}
|
||||
|
||||
// 4. 如果代码里面没有返回对象,那么返回该 this
|
||||
// return this;
|
||||
}
|
||||
const huawei = new Computer("华为", 5000);
|
||||
console.log(huawei);
|
||||
```
|
||||
|
||||
**这其实就是 JS 中函数二义性的由来**。
|
||||
|
||||
不过,不管 JS 如何模拟面向对象的特性,哪怕 ES6 甚至新增了 class 关键字:
|
||||
|
||||
```js
|
||||
class Computer {
|
||||
constructor(name, price) {
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
const huawei = new Computer("华为", 5000);
|
||||
console.log(huawei);
|
||||
```
|
||||
|
||||
JS 底层仍然是一门基于原型的语言,这一点是不会改变的。现在不会变,未来,也不会变。
|
||||
|
||||
## 原型对象与原型链
|
||||
|
||||
### 1. 三角关系
|
||||
|
||||
假设对象是由构造函数生产的,前面我们说过,那只是表象,只是模拟,最终底层仍然采用的是原型的方式。并且构造函数、实例对象以及原型对象这三者之间,还存在一个著名的三角关系,如下图所示:
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-05-07-090308.png" alt="image-20240507170307699" style="zoom:50%;" />
|
||||
|
||||
这里的三角关系指的是:
|
||||
|
||||
1. 构造函数
|
||||
2. 实例对象
|
||||
3. 原型对象
|
||||
|
||||
这三者之间的关系。**在 JS 中,只要是由构造函数 new 出来的对象,都满足这样的关系,不管你是自定义构造函数还是内置的构造函数**。
|
||||
|
||||
```js
|
||||
function Computer(){}
|
||||
const c = new Computer();
|
||||
console.log(c.__proto__ === Computer.prototype);
|
||||
console.log(c.constructor === Computer);
|
||||
console.log(c.constructor === Computer.prototype.constructor);
|
||||
console.log("-------");
|
||||
console.log([].__proto__ === Array.prototype);
|
||||
console.log([].constructor === Array);
|
||||
console.log("-------");
|
||||
console.log(1..__proto__ === Number.prototype);
|
||||
console.log(1..constructor === Number);
|
||||
console.log("-------");
|
||||
console.log(true.__proto__ === Boolean.prototype);
|
||||
console.log(true.constructor === Boolean);
|
||||
```
|
||||
|
||||
### 2. 原型链全貌图
|
||||
|
||||
整个原型链的全貌图如下:
|
||||
|
||||

|
||||
|
||||
- JS 中的对象大体上分为两大类:**普通对象** 和 **构造器对象**
|
||||
|
||||
- 无论是 **普通对象** 还是 **构造器对象**,都会有自己的原型对象,通过 \__proto__ 这个隐式属性,就能找到自己的原型对象,并且**一直向上找,最终会到达 null.**
|
||||
- **普通对象** 和 **构造器对象** 的区别在于是否能够实例化,**构造器对象**可以通过 new 的形式创建新的实例对象,这些实例对象的原型对象一直往上找最终仍然是到达 null.
|
||||
- 只有 **构造器对象** 才有 prototype 属性,其 prototype 属性指向实例对象的原型对象
|
||||
- 所有 **构造器对象** 的原型对象均为 Function.prototype
|
||||
- 无论是 **普通对象** 还是 **构造器对象**,最终的 constructor 指向 Function,而 Function 的 constructor 指向自己本身。
|
||||
- Object 这个 **构造器对象** 比较特殊,实例化出来的对象的原型对象直接就是 Object.prototype,而其他的构造器对象,其实例对象的原型对象为对应的 xxx.prototype,再往一层才是 Object.prototype.
|
||||
|
||||
### 3. 原型链实际应用
|
||||
|
||||
学习原型相关的知识有什么用?
|
||||
|
||||
其实你只有了解了原型,你才能深刻的理解为什么方法要挂在原型对象上面。
|
||||
|
||||
例如:
|
||||
|
||||
```js
|
||||
function Computer(name, price) {
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
Computer.prototype.showPrice = function () {
|
||||
console.log(`${this.name}的电脑价格为${this.price}`);
|
||||
};
|
||||
|
||||
const huawei = new Computer("华为", 5000);
|
||||
const apple = new Computer("苹果", 8000);
|
||||
```
|
||||
|
||||
之所以要挂在原型对象上面,是因为由**构造函数实例化出来的每一个实例对象,属性值是不相同的,所以需要每个对象独立有一份**。
|
||||
|
||||
但是**对于方法而言,所有对象都是相同的,因此我们不需要每个对象拥有一份,直接挂在原型对象上面共用一份即可**。
|
||||
|
||||
如下图所示:
|
||||
|
||||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-08-24-072905.png" alt="image-20220824152905132" style="zoom: 45%;" />
|
||||
|
||||
你现在也就能够理解,为什么所有的构造函数内置方法都是挂在原型对象上面的。
|
||||
|
||||
例如:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array
|
||||
|
||||
另外,虽然我们能够轻松的给内置的构造器函数添加属性和方法:
|
||||
|
||||
```js
|
||||
Number.prototype.isEven = function () {
|
||||
return this % 2 === 0;
|
||||
}
|
||||
Number.prototype.isOdd = function () {
|
||||
return this % 2 === 1;
|
||||
}
|
||||
const i = 42;
|
||||
console.log(i.isEven()); // true
|
||||
const j = 13;
|
||||
console.log(j.isOdd()); // true
|
||||
```
|
||||
|
||||
但是**目前 JS 社区的大部分人都不推荐这么做,这样的做法往往被称之猴子补丁(monkey-patching)**
|
||||
|
||||
大部分人的观点是“别耍流氓,不是你的对象别动手动脚”。
|
||||
|
||||
一种更好的最佳实践是继承想要修改的构造函数,在子类上面添加新的方法:
|
||||
|
||||
```js
|
||||
class myNum extends Number{
|
||||
constructor(...args){
|
||||
super(...args);
|
||||
}
|
||||
zhangsan(){}
|
||||
}
|
||||
const i = new myNum(1);
|
||||
i.zhangsan();
|
||||
```
|
||||
|
||||
## 原型链相关方法
|
||||
|
||||
**1. Object.getPrototypeOf( )**
|
||||
|
||||
该方法用于查找一个对象的原型对象。
|
||||
|
||||
```js
|
||||
function Computer(){}
|
||||
const c = new Computer();
|
||||
console.log(Object.getPrototypeOf(c) === c.__proto__);
|
||||
```
|
||||
|
||||
**2. instanceof 操作符**
|
||||
|
||||
判断一个对象是否是一个构造函数的实例。如果是返回 *true*,否则就返回 *false*
|
||||
|
||||
```js
|
||||
function Computer(){}
|
||||
const c = new Computer();
|
||||
console.log(c instanceof Computer); // true
|
||||
console.log(c instanceof Array); // false
|
||||
console.log([] instanceof Array); // true
|
||||
```
|
||||
|
||||
**3. isPrototypeOf( )**
|
||||
|
||||
主要用于检测一个对象是否是一个另一个对象的原型对象,如果是返回 true,否则就返回 false
|
||||
|
||||
```js
|
||||
function Computer(){}
|
||||
const c = new Computer();
|
||||
console.log(Computer.prototype.isPrototypeOf(c)); // true
|
||||
console.log(Computer.prototype.isPrototypeOf([])); // false
|
||||
console.log(Array.prototype.isPrototypeOf([])); // true
|
||||
```
|
||||
|
||||
**4. hasOwnProperty( )**
|
||||
|
||||
判断一个属性是定义在对象本身上面还是从原型对象上面继承而来的。
|
||||
|
||||
如果是本身的,则返回 true,如果是继承而来的,则返回 false
|
||||
|
||||
```js
|
||||
const person = {
|
||||
arm: 2,
|
||||
legs: 2,
|
||||
walk() {
|
||||
console.log("walking");
|
||||
},
|
||||
};
|
||||
|
||||
const john = Object.create(person, {
|
||||
name: {
|
||||
value: "John",
|
||||
enumerable: true,
|
||||
},
|
||||
age: {
|
||||
value: 18,
|
||||
enumerable: true,
|
||||
},
|
||||
});
|
||||
console.log(john.hasOwnProperty("name")); // true
|
||||
console.log(john.hasOwnProperty("arms")); // false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
-EOF-
|
||||
118
06. 原型链/课堂代码/demo/index.js
Normal file
118
06. 原型链/课堂代码/demo/index.js
Normal file
@ -0,0 +1,118 @@
|
||||
// const obj = {};
|
||||
// console.log(obj.__proto__);
|
||||
|
||||
// const person = {
|
||||
// arm: 2,
|
||||
// legs: 2,
|
||||
// walk() {
|
||||
// console.log("walking");
|
||||
// },
|
||||
// };
|
||||
|
||||
// const john = Object.create(person, {
|
||||
// name: {
|
||||
// value: "John",
|
||||
// enumerable: true,
|
||||
// },
|
||||
// age: {
|
||||
// value: 18,
|
||||
// enumerable: true,
|
||||
// },
|
||||
// });
|
||||
// console.log(john.__proto__ === person); // true
|
||||
|
||||
// function Computer(name, price) {
|
||||
// // 1. 创建一个普通的对象
|
||||
// // const obj = {};
|
||||
|
||||
// // 2. 设置该对象的原型对象
|
||||
// // obj.__proto__ = Computer.prototype;
|
||||
|
||||
// // 3. 设置 this 的指向,指向该 obj
|
||||
// // this ---> obj
|
||||
// this.name = name; // {name: "华为"}
|
||||
// this.price = price; // {name: "华为", price: 5000}
|
||||
|
||||
// // 4. 如果代码里面没有返回对象,那么返回该 this
|
||||
// // return this;
|
||||
// }
|
||||
// const huawei = new Computer("华为", 5000);
|
||||
// console.log(huawei);
|
||||
|
||||
// class Computer {
|
||||
// constructor(name, price) {
|
||||
// this.name = name;
|
||||
// this.price = price;
|
||||
// }
|
||||
// }
|
||||
// const huawei = new Computer("华为", 5000);
|
||||
// console.log(huawei);
|
||||
|
||||
// function Computer(){}
|
||||
// const c = new Computer();
|
||||
// console.log(c.__proto__ === Computer.prototype);
|
||||
// console.log(c.constructor === Computer);
|
||||
// console.log(c.constructor === Computer.prototype.constructor);
|
||||
// console.log("-------");
|
||||
// console.log([].__proto__ === Array.prototype);
|
||||
// console.log([].constructor === Array);
|
||||
// console.log("-------");
|
||||
// console.log(1..__proto__ === Number.prototype);
|
||||
// console.log(1..constructor === Number);
|
||||
// console.log("-------");
|
||||
// console.log(true.__proto__ === Boolean.prototype);
|
||||
// console.log(true.constructor === Boolean);
|
||||
|
||||
// function Computer(){}
|
||||
// const c = new Computer();
|
||||
// console.log(c.__proto__.__proto__.__proto__)
|
||||
// console.log(Computer.constructor.constructor);
|
||||
// console.log(Computer.__proto__ === Function.prototype);
|
||||
// new Computer.prototype();
|
||||
// console.log(Computer.prototype.prototype);
|
||||
|
||||
// console.log(c.__proto__.__proto__ === {}.__proto__);
|
||||
|
||||
// const result = 1..toFixed(3);
|
||||
// console.log(result, typeof result);
|
||||
// // Number.prototype.zhangsan = function(){}
|
||||
// class myNum extends Number{
|
||||
// constructor(...args){
|
||||
// super(...args);
|
||||
// }
|
||||
// zhangsan(){}
|
||||
// }
|
||||
// const i = new myNum(1);
|
||||
// i.zhangsan();
|
||||
|
||||
// function Computer() {}
|
||||
// const c = new Computer();
|
||||
// // console.log(Object.getPrototypeOf(c) === c.__proto__);
|
||||
// // console.log(c instanceof Computer);
|
||||
// // console.log(c instanceof Array);
|
||||
// // console.log([] instanceof Array);
|
||||
|
||||
// console.log(Computer.prototype.isPrototypeOf(c)); // true
|
||||
// console.log(Computer.prototype.isPrototypeOf([])); // false
|
||||
// console.log(Array.prototype.isPrototypeOf([])); // true
|
||||
|
||||
const person = {
|
||||
arm: 2,
|
||||
legs: 2,
|
||||
walk() {
|
||||
console.log("walking");
|
||||
},
|
||||
};
|
||||
|
||||
const john = Object.create(person, {
|
||||
name: {
|
||||
value: "John",
|
||||
enumerable: true,
|
||||
},
|
||||
age: {
|
||||
value: 18,
|
||||
enumerable: true,
|
||||
},
|
||||
});
|
||||
console.log(john.hasOwnProperty("name")); // true
|
||||
console.log(john.hasOwnProperty("arms")); // false
|
||||
Loading…
x
Reference in New Issue
Block a user