变量类型和计算
JavaScript有哪些数据类型,它们的区别?
1、JavaScript共有8种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表后独一无二且不可变的数据类型,为了解决全局变量冲突问题。
- BigInt:表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围(2^53 - 1)。
双精度数64位,1位符号,11指数,53位小数。
2、这些数据可以分为值类型和引用类型:
- 栈:值类型:Undefined、Boolean、Number、String、Symbol、BigInt
- 堆:引用类型:对象、数组和函数
- Null:特殊的引用类型,指向空指针
- 函数:是特殊的引用类型,但不用于存储数据,所以没有拷贝,复制函数这一说。
- 栈从上往下放,堆从下往上放。
数据类型检测的方式有哪些
- typeof:能判断原始类型、函数,原始类型的null和其他引用类型会被判断成object。
- instanceof:可以正确判断引用数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。不稳定,把类的原型进行重写,在重写的过程中很有可能出现把之前的 constructor 给覆盖了。
- constructor:能判断原始类型和引用类型,也不稳定。
- Object.prototype.toString.call():能判断所有类型。
- 其他单独的检测类型:Array.isArray。
什么是 JavaScript 中的包装类型
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象实例,并在创建后立即销毁实例。如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
在访问'abc'.length
时,JavaScript 将’abc’在后台转换成String('abc')
,然后再访问其length属性。
使用
Object
函数显式地将基本类型转换为包装类型var a = 'abc'
Object(a) // String {"abc"}
使用
valueOf
方法将包装类型倒转成基本类型:var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
包装对象还可以自定义方法和属性,供原始类型的值直接调用 ```javascript //我们可以新增一个double方法,使得字符串和数字翻倍
String.prototype.double = function () { return this.valueOf() + this.valueOf(); };
‘abc’.double() // abcabc
<a name="HsuaJ"></a>
## JavaScript 中如何进行隐式类型转换?
**JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。**<br />**这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用**`**ToPrimitive**`**转换成基本类型,再进行操作。**<br />`ToPrimitive`方法, JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。
下面是不同类型的值转原始类型的规则:<br />**1、原始类型转原始类型**<br />(1)转`number`:依次执行`Number()`。**如果有一个字符不是数字,返回**`**NaN**`。
- `** undefined**`**转成 **`**NaN**`
- `**null **`**转成 0 **
- `boolean`转 0/1
- `string`转成对于`number`,无法转的话就是`NaN`
(2)转`string`:
- `undefined `👉` 'undefined'`
- `null` 👉` 'null'`
- `number `👉` 'number'`
- `boolean` 👉 `'true'/'false'`
**2、如果值为引用类型转原始类型**<br />执行`ToPrimitive(input[, PreferredType])`,`type`的值为`number`或者`string`。<br />**(1)当**`type`**为**`number`**时规则如下:**
- 调用`obj`的`valueOf`方法,如果为原始值,则返回,否则下一步;
- 调用`obj`的`toString`方法,后续同上;
- 抛出`TypeError `异常。
**(2)当**`type`**为**`string`**时规则如下:**
- 调用`obj`的`toString`方法,如果为原始值,则返回,否则下一步;
- 调用`obj`的`valueOf`方法,后续同上;
- 抛出`TypeError `异常。
**可以看出两者的主要区别在于调用**`**toString**`**和**`**valueOf**`**的先后顺序。**默认情况下:
- 如果对象为` Date `对象,则`type`默认为`string`;
```javascript
new Date(2017, 4, 21).valueOf() // 1495296000000
- 其他情况下,
**type**
默认为**number**
。
总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:
var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN
+操作符的隐式类型转换过程
- 一元操作符:会调用
Number()
处理该值,相当于 Number(‘1’),最终结果返回数字 1。 ```javascript console.log(+[‘1’]); // 1
console.log(+[‘1’, ‘2’, ‘3’]); // NaN
console.log(+{}); // NaN
2. **二元操作符 :两边有至少一个**`**string**`**类型变量时,两边的变量都会被隐式转换为字符串**;**其他情况下两边的变量都会被转换为数字。**
```javascript
// 两边都会转成number
// null是基本类型,返回Number(null) = 0已经是基本类型了,不用再转。
null + 1 // 1
1 + '23' // '123'
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
// 两边都不是string,所以强制类型转换成数字。ToPrimitive([],number)
// [].valueOf() = [] -> [].toString() = ''
// [].toString() = "[object Object]"
[] + {} // "[object Object]"
== 操作符的强制类型转换过程
操作符两边的值都尽量转成**number**
- 类型相同,调用 === 操作符
- 类型不同
1、查看是否是** undefined **
和** null**
比较,是的话返回**true**
,不是下一步;
2、是否在比较string
和number
,是的话,把string
转换成number
。不是下一步;
3、查看比较项是否有boolean
,如果有,那么将 boolean
转为 number
并回到最初重新比较。不是下一步
📢:string
和boolean
遇到number
都是转number
4、查看是否有一项是object
,如果有,那么将 object
转为其原始值 Number
。
<和>比较符
- 两边都是字符串,则比较字母表顺序
-
|| 和&&操作符的返回值
对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。
Boolean()
undefined
👉 falsenull
👉 falsenumber
👉 当为 0 时 false 否则为 truestring
👉 当为空字符串时为 false 否则为 true**object**
、**array **``**Date **
👉 true:所有对象(包括数组和函数)都转换为true
三种number转换方法的区别
Number()
:如果有一个字符不是数字,结果都会返回 NaNNumber("1.2") // 1.2
Number("100a") // NaN
parseInt
:和parseFloat
都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回 NaN。 ```javascript parseInt(“1q”) //1
parseInt(“q1”) //NaN
- `parseFloat`
```javascript
parseFloat(".1") // 0.1
parseFloat("3.14 abc") // 3.14
数据处理
console.log(017 - 011)
console.log(018 - 011)
console.log(019 - 011)
Object.is() 与比较操作符 “===”、“==” 的区别
Object.is 一
般情况下和三等号的判断相同,但它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
为什么0.1+0.2 ! == 0.3
1、因为浮点数存储时的精度丢失和运算时的精度丢失,导致0.1 + 0.2 !==0.3。
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1和0.2这两个数的二进制都是无限循环的数。
JavaScript采用双精度数(64位)来保存浮点数:1位符号,11位指数,52位小数,存不下的小数遵从“0舍1入”的原则,这是第一次精度丢失。
二进制浮点数相加的过程中,小数位相加导致小数位多出了一位,又要让第53位的数进行为1则进1为0则舍去的操作,又造成一次精度丢失。
这两次精度丢失导致0.1 + 0.2 !==0.3。
2、如何解决
- 设置一个误差范围:在ES6中,提供了
Number.EPSILON
属性。只要判断0.1+0.2-0.3是否小于Number.EPSILON
,如果小于,就可以判断为0.1+0.2 ===0.3 ```javascript function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
- 实际中处理好精度丢失的库`number-precision`:思路是分别把小数转换成整数再运算
<a name="p7EFr"></a>
## map和Object的区别
| **<br /> | **Map** | **Object** |
| --- | --- | --- |
| 意外的键 | Map默认情况不包含任何键,只包含显式插入的键。 | **Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。** |
| 键的类型 | Map的键可以是任意值,包括函数、对象或任意基本类型。 | Object 的键必须是 **String 或是Symbol**。 |
| 键的顺序 | Map 中的 key 是**有序**的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 | Object 的键是无序的 |
| Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 |
| 迭代 | **Map 是 iterable 的,所以可以直接被迭代。** | 迭代Object需要以某种方式获取它的键然后才能迭代。 |
| 性能 | **在频繁增删键值对的场景下表现更好。** | 在频繁添加和删除键值对的场景下未作出优化。 |
<a name="YUosx"></a>
## map和weakMap的区别
**(1)Map**<br />map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。<br />**Map数据结构有以下操作方法:**
- **size**: map.size 返回Map结构的成员总数。
- **set(key,value)**:设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
- **get(key)**:该方法读取key对应的键值,如果找不到key,返回undefined。
- **has(key)**:该方法返回一个布尔值,表示某个键是否在当前Map对象中。
- **delete(key)**:该方法删除某个键,返回true,如果删除失败,返回false。
- **clear()**:map.clear()清除所有成员,没有返回值。
Map结构原生提供是三个遍历器生成函数和一个遍历方法
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
- forEach():遍历Map的所有成员。
**(2)WeakMap**<br />WeakMap 对象也是一组键值对的集合,其中的**键是弱引用的**。**其键必须是对象**,原始数据类型不能作为key值,而值可以是任意的。<br />WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。<br />而WeakMap的**键名所引用的对象都是弱引用**,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的**键名对象和所对应的键值对会自动消失,不用手动删除引用**。
<a name="WnWzb"></a>
## 数组有哪些原生方法?
- 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
- 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
- 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
- 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
- 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
- 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
- 数组归并方法 reduce() 和 reduceRight() 方法
- 扁平化数组:flat、flatMap
<a name="qBXqP"></a>
## for...in和for...of的区别
**1、遍历的对象不同**<br />`**for...in **`循环主要是为了遍历可枚举数据,如对象、数组、字符串,包括原型链上可枚举的属性。
```javascript
Object.getOwnPropertyDescriptor({x:1},'x')
// {value: 1, writable: true, enumerable: true, configurable: true}
for…of 循环用来遍历可迭代数据,如数组、字符串、Map、Set数组、类数组对象,字符串、Set、Map 以及 Generator 对象,遍历普通对象会报错。
arr[Symbol.iterator]()
如何使用for…of遍历对象
for…of
是作为ES6新增的遍历方式,允许遍历一个含有iterator
接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of
遍历是会报错的。
1、类数组对象:用Array.from
转成数组。
var obj = {
0:'one',
1:'two',
length: 2
};
obj = Array.from(obj);
for(var k of obj){
console.log(k)
}
2、不是类数组对象,就给对象添加一个**[Symbol.iterator]**
属性,并指向一个迭代器即可。
// 方法二
var obj = {
a:1,
b:2,
c:3
};
obj[Symbol.iterator] = function*(){
var keys = Object.keys(obj);
for(var k of keys){
yield [k,obj[k]]
}
};
for(var [k,v] of obj){
console.log(k,v);
}
for循环、forEach和map方法有什么区别
这方法都是用来遍历数组的,两者区别如下:
- forEach()方法会针对每一个元素执行提供的函数,该方法没有返回值,是否会改变原数组取决于数组元素的类型是基本类型还是引用类型,详细解释可参考文章:《forEach到底可以改变原数组吗》
map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
for循环、forEach的区别
性能:for > forEach > map
for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。
- 对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。
- map 最慢的原因是因为 map 会返回一个新的数组,数组的创建和赋值会导致分配内存空间,因此会带来较大的性能开销。如果将map嵌套在一个循环中,便会带来更多不必要的内存消耗。
- forEach并不支持
break
、return
、continue
这三种中断循环的操作,使用`会导致报错,需要结合
try…catch()`跳出循环。for循环可以。 - for循环过程中支持修改索引(修改 i),但forEach做不到(底层控制index自增,我们无法左右它),index不会随着函数体内部对它的增减而发生变化。
let arr = [1, 2];
arr.forEach((item, index) => {
arr.splice(index, 1);
console.log(1); //输出几次?
});
console.log(arr) //[2]
手写深拷贝
函数
JavaScript为什么要进行变量提升,它导致了什么问题?
变量提升指的是在词法分析阶段,在内存开辟空间,存放变量和函数。
1、原因:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间。
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
2、变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
执行上下文
JS执行一段代码前,会先创建对应的执行上下文, 把上下文压入调用栈, 再进入执行上下文,执行代码。执行上下文包含三个属性:
- 变量对象(VO) ,包含:函数的所有形参、 函数声明、变量声明。
- 作用域(Scope chain) :Scope: [AO, [[Scope]]]
- this
函数执行完毕,对应的执行上下文出从栈顶弹出,函数的作用域也会随之销毁,其包含的变量会被统一释放并触发垃圾回收机制回收。执行上下文分为三种:
- 全局执行上下文:首次执行JS代码时,会创建一个全局上下文。this执行全局对象。
- 函数执行上下文:在函数被调用时创建。每次调用函数都会创建一个新的执行上下文。
eval执行上下文。
eval('var x = 10');
🔗作用域和作用域链
作用域是可访问变量的集合,作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。JavaScript采用的是词法作用域,也就是在函数定义时,函数的作用域就决定了。具体过程是:
1、函数有个内部属性[[scope]],当函数创建时,就会保存所有的父变量到其中。
2、当函数被激活时,进入函数的执行上下文,创建活动对象VO/AO后,会把活动对象添加到作用域链的前端。至此,作用域链创建完毕。也就是:Scope: [AO, [[Scope]]]。作用域分为以下三种:块级作用域:存在于函数内部,块中(字符{}之间的区域)。let 和const属于块级声明,块级作用域以外不可以访问。有以下特征:
- 不会被提升
- 重复声明报错
- 不绑定全局作用
- 函数作用域
-
对this的理解
this是当前执行上下文的一个属性,this是函数运行时绑定的,它的指向由是函数的调用方式决定的。函数的调用方式有4种:
作为函数调用(默认绑定):非严格模式默认是window ,严格模式
undefined
- 作为对象的方法调用(隐式绑定): this指向最后调用它的对象。
- 使用构造函数调用(new绑定):指向实例对象,new绑定。
- 作为函数方法调用(call、apply)显式绑定:指向第一个参数,如果是null或undefined,将使用全局对象替代。
优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
this输出题目
call() 和 apply() 的区别?
实现call、apply 及 bind 函数
(1)call 函数的实现步骤:
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 处理传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性。
返回结果。
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
**(2)apply 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性
返回结果
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};
(3)bind 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回
函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
箭头函数
this由定义时它的外层作用域的this决定。
箭头函数的this
无法通过bind\call\apply
修改,只能通过改变作用域中的this来间接修改。
1、箭头函数特点
(1)箭头函数没有自己的this对象(详见下文)。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用rest 参数
代替。
(4)不可以使用yield
命令,因此箭头函数不能用作Generator
函数。
2、避免使用箭头函数的场景定义对象的方法
- 定义对象原型方法
- 构造函数使用箭头函数
- 动态上下文的回调函数,比如作为事件的回调函数
- Vue生命周期函数,因为Vue本质上是个大对象。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
原理是基于函数作用域链的规则和垃圾回收机制的引用计数规则。
- 函数有个内部属性
[[scope]]
,当函数创建时,就会保存所有的父变量到其中。 - 闭包执行时,其父级上下文已经销毁了,但是因为闭包作用域包含对父级变量的引用,所以这个变量不会被垃圾回收机制回收。
【优点】是私有化数据,在私有化数据的基础上保持数据。
【缺点】使用不恰当会导致内存泄漏,在不需要的时候要及时把闭包函数重置为null。
【应用】闭包的应用非常广泛,例如我们常见的节流,防抖,函数柯里化。
webpack整体是一个自执行的函数,利用了闭包的特性,保证了内部变量私有化, 同时也不会对全局变量造成污染。
同时我们在这个自执行函数中设置一个对象来模拟并保存模块中exports的内容。同时声明一个函数来模拟 require方法。 每一个模块的执行也都放到一个自执行函数中。
!!手写函数柯里化
原型和原型链
对原型、原型链的理解
1、原型(prototype
)定义了实例的共享方法。每个对象都有一个原型指针__proto__
,指向构造函数的原型prototype
,并从原型继承方法和属性。
2、访问对象的属性时,会在对象的原型指针__proto__
去找,也就是去构造函数的原型__proto__
去找。原型也有可能有自己的原型,也有__proto__
指针,指向它的构造函数的原型。这样一层一层,最终指向null
。这样的关系,被称作原型链。
function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false
prototype属性和proto的区别
1、protot:
每个对象都有属性__ptoto_
指针,指向该对象的构造函数的prototype
。访问属性时,访问不到,就会在对象的原型__proto__
去找,也就是去构造函数的prototype去找。
2、prototype描述的是函数的原型对象,函数除了有proto属性外,还有prototype属性。指向该方法的原型对象。
- 实例对象有可能没有原型prototype。
prototype还有constructor属性指向构造函数。
intanceof 操作符的实现原理及实现
instanceof
运算符用于判断构造函数的** prototype **
属性是否出现在目标对象的原型链中的任何位置。function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
原型链指向
p.__proto__ // Person.prototype
Person.prototype.__proto__ // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor // Person
原型链的终点是什么?如何打印出原型链的终点?
由于
Object
是构造函数,原型链终点是Object.prototype.__proto__
,而Object.prototype.__proto__=== null // true
,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype
的下一级是Object.prototype.__proto__
。new操作符的实现原理
new操作符的执行过程:
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 判断参数是否是一个函数
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
result = constructor.apply(newObject, arguments);
// 判断返回对象
let flag = result && (typeof result === "object" || typeof result === "function");
// 判断返回结果
return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);
类
继承的本质
1、继承父类的原型
2、继承父类的静态方法
3、执行父类构造器的方法
8种继承方式
1、原型链继承:重写子类的原型对象,把它赋值为父级的构造函数。
优点:继承了父类的模板,又继承了父类的原型对象。
缺点:在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱
还有就是在创建子类型的时候不能向超类型传递参数。
function Parent () {
this.name = 'Parent'
this.sex = 'boy'
}
function Child () {
this.name = 'child'
}
// 将子类的原型对象指向父类的实例
Child.prototype = new Parent()
2、构造函数继承:在子类型的构造函数内部调用父类的构造函数
缺点:继承父类的实例属性和方法,不能继承父类原型的属性和方法
function Child (name,age){
//第一次调用Father:构造函数继承,实例属性和方法
Father.call(this,name)
this.age = age
}
3、组合继承:将原型链继承与构造函数继承组合在一起。
缺点:使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。
4、原型继承:向函数中传入一个对象,然后返回一个以这个对象为原型的对象。
缺点与原型链方式相同。
function object (o){
function F(){}
F.prototype = o
return new F()
}
5、寄生式继承:和工厂模式类似,即创建一个用于封装继承过程的函数。该函数在内部以某种方式来增强对象,最后像是他做了所有工作一个返回对象。
生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。
function createAnother(original){
//通过调用object函数创建一个新对象
var clone = object(original);
clone.sayHi = function(){//以某种方式来增强这个对象
alert("hi");
};
return clone;//返回这个对象
}
6、寄生组合继承:调用父类的构造函数继承父类属性,通过父类原型链的混成形式来继承父类方法。
// 父类
function SuperType (name) {
this.colors = ["red", "blue", "green"];
this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
return this.name;
};
// 子类
function SubType (name, subName) {
// 调用 SuperType 构造函数,继承实例属性-
SuperType.call(this, name);
this.subName = subName;
};
class继承:extends关键字继承父类原型链的属性,super方法调用:继承父类的属性,Father.call(this, name)
extends原理
要点1:将子类的原型prototype赋值为父类的原型prototype。Child.prototype._proto__= Father.prototype
要点2:将子类的原型指针proto赋值为父类Child._proto__= Father
function _inherits(subType, superType) {
// 创建对象,Object.create 创建父类原型的一个副本
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: { // 重写 constructor
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
// Object.setPrototypeOf 是__proto__的兼容写法
if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}
ES5和ES6继承的区别
ES5:先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
ES6:创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。
继承子类的prototype属性和proto
子类的__proto__
属性,表示构造函数的继承,总是指向父类。Child.__proto__ === Father
子类prototype的__proto__
,指向父类的prototype
属性。Child.prototype.__proto__ === Father.prototype