0%

JavaScript核心原理

JS数据类型

概念

JavaScript 的数据类型有下图所示的 8 种:
8种类型
前 7 种类型为基础类型,最后 1 种(Object)为引用类型,而引用数据类型(Object)又分为图上这几种常见的类型:Array - 数组对象、RegExp - 正则对象、Date - 日期对象、Math - 数学函数、Function - 函数对象。
各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中,因此上面的数据类型大致可以分成两类来进行存储:

  • 基础类型存储在栈内存,被引用或拷贝时,会创建一个完全相等的变量;
  • 引用类型存储在堆内存,存储的是地址,多个引用指向同一个地址,这里会涉及一个“共享”的概念。

类型检测

判断方法1:typeof
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typeof 1 // 'number'

typeof '1' // 'string'

typeof undefined // 'undefined'

typeof true // 'boolean'

typeof Symbol() // 'symbol'

typeof null // 'object'

typeof [] // 'object'

typeof {} // 'object'

typeof console // 'object'

typeof console.log // 'function'

typeof对于基本类型,除了 null 都可以显示正确的类型,虽然null是基本类型,但是会显示object,这是一个存在很久的Bug,因为在 JS 的最初版本中,使用的是 32位系统,为了性能考虑使用低位存储了变量的类型信息,000开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

判断方法2:instanceof

我们new一个对象,那么这个新对象就是它原型链继承上面的对象了,通过instanceof我们能判断这个对象是否是之前那个构造函数生成的对象,这样就基本可以判断出这个新对象的数据类型。下面通过代码来了解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
let Car = function() {}

let benz = new Car()

benz instanceof Car // true

let car = new String('Mercedes Benz')

car instanceof String // true

let str = 'Covid-19'

str instanceof String // false

那么如何实现一个instanceof的底层实现呢,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function myInstanceof(left, right) {

// 这里先用typeof来判断基础数据类型,如果是,直接返回false

if(typeof left !== 'object' || left === null) return false;

// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象

let proto = Object.getPrototypeOf(left);

while(true) { //循环往下寻找,直到找到相同的原型对象

if(proto === null) return false;

if(proto === right.prototype) return true;//找到相同原型对象,返回true

proto = Object.getPrototypeof(proto);

}
}
// 验证一下自己实现的myInstanceof是否OK
console.log(myInstanceof(new Number(123), Number)); // true
console.log(myInstanceof(123, Number)); // false

前面两种判断数据类型的方法它们之间有什么差异呢?我总结了下面两点:

  • instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型;

  • 而 typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判断。

判断方法3:Object.prototype.toString

toString()Object的原型方法,调用该方法,可以统一返回格式为“[object Xxx]”的字符串,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object];而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"

下面来实现一个全局通用的数据类型判断方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); // 注意正则中间有个空格
}

/* 代码验证,需要注意大小写,哪些是typeof判断,哪些是toString判断?思考下 */
getType([]) // "Array" typeof []是object,因此toString返回
getType('123') // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null) // "Null"首字母大写,typeof null是object,需toString来判断
getType(undefined) // "undefined" typeof 直接返回
getType() // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小写
getType(/123/g) //"RegExp" toString返回

深浅拷贝

浅拷贝

创建一个新的对象,来接收你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

上述例子看出,给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变,如果不想要改变,可以用浅拷贝的方式,下面就是浅拷贝的几种方法:

方法1:object.assign

object.assign是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,该方法有两个参数:
参数1:拷贝的目标对象
参数2:拷贝的来源对象(也可以是多个来源)

1
2
3
4
5
6
let a = {
age: 1
}
let b = Object.assign({},a)
a.age = 10;
console.log(b.age) // 1

使用 object.assign 方法有几点需要注意:

  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 可以拷贝 Symbol 类型的属性。
方法2:...扩展运算符方式
1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

扩展运算符object.assign有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

方法3:concat拷贝数组

语法:

1
arrayObject.concat(arrayX,arrayX,......,arrayX)

concat() 方法用于连接两个或多个数组,该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。注意:concat只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。

1
2
3
4
5
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
方法4:slice拷贝数组

语法:

1
arrayObject.slice(begin, end);

slice()方法可从已有的数组中返回选定的元素,该方法并不会修改数组,而是返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素,下面是实现浅拷贝的代码示例:

1
2
3
4
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]

从上面的代码中可以看出,浅拷贝只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力,为了解决这个问题,这个时候就可以采用深拷贝,它能解决多层对象嵌套问题,彻底实现拷贝。

手工实现浅拷贝

实现思路:

  • 对基础类型做一个最基本的一个拷贝;
  • 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const shallowClone = (target) => {
// 类型判断,基本类型直接返回,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}

hasOwnProperty()方法用来检测一个属性是否是对象的自有属性,而不是从原型链继承的。如果该属性是自有属性,那么返回true,否则返回 false。换句话说,hasOwnProperty()方法不会检测对象的原型链,只会检测当前对象本身,只有当前对象本身存在该属性时才返回true

1
2
3
4
5
6
7
let obj = {
name: 'zs',
age: 12
}
for (let prop in obj) {
console.log(obj.hasOwnProperty(prop)) // true
}

深拷贝

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

方法1:JSON.stringfy

JSON.stringify()是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为JSON的字符串,并将对象里面的内容转换成字符串,最后再用JSON.parse()的方法将JSON字符串生成一个新的对象。示例代码如下所示:

1
2
3
4
5
6
7
8
let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2); //{a:1,b:[1,2,3]}
obj1.a = 2
obj1.b.push(4);
console.log(obj1); //{a:2,b:[1,2,3,4]}
console.log(obj2); //{a:1,b:[1,2,3]}

该方法也是有局限性的:

  • 拷贝的对象的值中如果有函数undefinedsymbol这几种类型,经过JSON.stringify序列化之后的字符串中这个键值对会消失
  • 拷贝 Date 引用类型会变成字符串;
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  • 无法拷贝对象的循环引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 针对上面说法示例:
function Obj() {
this.func = function () { alert(1) };
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);

以上代码的打印结果:
打印结果
使用 JSON.stringify 方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify 暂时还是无法满足的,那么就需要下面的几种方法了。

方法2:基础版(手写递归实现)

下面是一个实现 deepClone 函数封装的例子,通过 for in 遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}

虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringfy 一样,还是有一些问题没有完全解决,例如:

  • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
  • 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
  • 对象的属性里面成环,即循环引用没有解决。

这种基础版本的写法也比较简单,可以应对大部分的应用情况。

方法3:改进版(改进后递归实现)

针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。

  • 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
  • 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
  • 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
  • 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。

那么针对上面这几个问题,我们来看下改进后的递归实现的深拷贝代码应该是什么样子的,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date)
return new Date(obj) // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp)
return new RegExp(obj) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
//继承原型链
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)

我们看一下结果,cloneObj 在 obj 的基础上进行了一次深拷贝,cloneObj 里的 arr 数组进行了修改,并未影响到 obj.arr 的变化,如下图所示。
展示结果
从这张截图的结果可以看出,改进版的 deepClone 函数已经对基础版的那几个问题进行了改进,也验证了我上面提到的那四点理论。

JS常见的6种继承方式

在学习继承之前,先了解原型链,便于更好的理解
原型链

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原型链继承
function Person (){
this.name = "zs";
this.age = 20;
this.play = [1,2,3]
}

function Child(){
this.type= 'child2'
}

Child.prototype = new Person();

console.log(new Child())

原型链继承方式也是有缺点的,如下代码:

1
2
3
4
var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]

因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化

构造函数继承(借助 call)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person(){
this.name = 'zs'
this.type= [1,2,3]
this.getAge = function(){
return `20岁`
}
}

Person.prototype.getName = function(){
return this.name
}

function Child(){
Person.call(this)
this.age = 100
}

let s = new Child()
let s2 = new Child()
s.type.push(4)
console.log(s,s2)
console.log(s.getName()) // 报错 s.getName is not a function
console.log(s.getAge()) // 20岁

从上面的结果就可以看到构造函数实现继承的优缺点,它使父类的引用属性不会被共享,优化了第一种继承方式的弊端;但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法,不能继承原型属性或者方法

组合继承(前两种组合)

这种方式结合了前两种继承方式的优缺点,结合起来的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(){
this.name = 'zs',
this.arr = [1,2,3]
}
Person.prototype.getName = function(){
return this.name
}
function Child(){
// 第二次调用 Person()
Person.call(this)
this.type = "child"
}
// 第一次调用 Person()
Child.prototype = new Person()
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child
let s = new Child()
let s2 = new Child()
s.arr.push(4)
console.log(s.arr,s2.arr) // [1, 2, 3, 4]  [1, 2, 3]
console.log(s.getName()); // zs
console.log(s2.getName()); // zs

组合继承打印的结果可以看出,上面原型链继承构造函数继承的问题都得以解决,但是有个新的问题是,Person执行了两次,第一次改变Childprototype的时候,第二次是通过call调用Person的时候,Person多构造一次就多进行一次性能开销,这是一个缺点。

原型式继承

通过ES5 里面的 Object.create方法,这个方法接收两个参数:参数1:用作新对象原型的对象、参数2:为新对象定义额外属性的对象(可选参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
}
};

let p = Object.create(parent4);
p.name = "tom";
p.friends.push("jerry");

let p2 = Object.create(parent4);
p2.friends.push("lucy");

console.log(p.name); // tom
console.log(p.name === p.getName()); // true
console.log(p.name); // parent4
console.log(p.friends); //  ["p1", "p2", "p3", "jerry", "lucy"]
console.log(p2.friends); //  ["p1", "p2", "p3", "jerry", "lucy"]

后两个输出结果是一样的,可以联想到上面的浅拷贝的知识点,关于引用数据类型“共享”的问题,其实Object.create方法是可以为一些对象实现浅拷贝的,关于这种继承方式的缺点也很明显,多个实例的引用类型属性指向相同的内存,存在篡改的可能。

寄生式继承

上面使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
}
};

function clone(original) {
let clone = Object.create(original);
clone.getFriends = function () {
return this.friends;
};
return clone;
}

let p = clone(parent5);

console.log(p.getName()); // parent5
console.log(p.getFriends()); // ["p1", "p2", "p3"]

通过上面这段代码可以看到 p 是通过寄生式继承生成的实例,它不仅仅有 getName 的方法,而且可以看到它最后也拥有了 getFriends 的方法,从最后的输出结果中可以看到,p 通过clone的方法,增加了 getFriends 的方法,从而使 p 这个普通对象在继承过程中又增加了一个方法,这样的继承方式就是寄生式继承。我在上面第三种组合继承方式中提到了一些弊端,即两次调用父类的构造函数造成浪费,下面要介绍的寄生组合继承就可以解决这个问题。

寄生组合式继承

结合上面寄生式继承中提及的继承方式,解决普通对象的继承问题的Object.create方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}

function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function () {
return this.friends;
}

let person6 = new Child6();
console.log(person6); // {friends: "child5",name: "parent6",play: (3) [1, 2, 3]}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

通过这段代码可以看出来,这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销,可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题,可以输出预期的结果。
整体看下来,这六种继承方式中,寄生组合式继承是这六种里面最优的继承方式。另外,ES6 还提供了继承的关键字 extends,我们再看下 extends 的底层实现继承的逻辑。

ES6的extends继承

利用 ES6 里的 extends 的语法糖,使用关键词很容易直接实现 JavaScript 的继承,但是如果想深入了解 extends 语法糖是怎么实现的,就得深入研究 extends 的底层逻辑,我们先看下用利用 extends 如何直接实现继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

因为浏览器的兼容性问题,如果遇到不支持 ES6 的浏览器,那么就得利用 babel 这个编译工具,将 ES6 的代码编译成 ES5,让一些不支持新语法的浏览器也能运行。
那么最后 extends 编译成了什么样子呢?我们看一下转译之后的代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function _possibleConstructorReturn (self, call) { 
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits (subClass, superClass) {
// 这里可以看到
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

var Parent = function Parent () {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));

从上面编译完成的源码中可以看到,它采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式。

实现new、apply、call、bind的底层逻辑

new原理介绍

new 关键词的主要作用就是执行一个构造函数、返回一个实例对象,在 new 的过程中,根据构造函数的情况,来确定是否可以接受参数的传递

1
2
3
4
5
function Person(){
this.name = 'Jack';
}
var p = new Person();
console.log(p.name) // Jack

大致分为以下几个步骤:

  • 创建一个新对象;
  • 将构造函数的作用域赋给新对象(this 指向新对象);
  • 执行构造函数中的代码(为这个新对象添加属性);
  • 返回新对象。

下面对构造函数稍微改动一下,直接return看下会发生什么变化

1
2
3
4
5
6
7
8
function Person(){
this.name = 'Jack';
return {age: 18}
}
var p = new Person();
console.log(p) // {age: 18}
console.log(p.name) // undefined
console.log(p.age) // 18

当构造函数最后 return 出来的是一个和 this 无关的对象时,new 命令会直接返回这个新对象,而不是通过 new 执行步骤生成的 this 对象。

1
2
3
4
5
6
7
function Person(){
this.name = 'Jack';
return 'tom';
}
var p = new Person();
console.log(p) // {name: 'Jack'}
console.log(p.name) // Jack

当构造函数中 return 的不是一个对象时,那么它还是会根据 new 关键词的执行逻辑,生成一个新的对象(绑定了最新 this),最后返回出来。

总结:new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象。

apply & call & bind 原理介绍

call、apply 和 bind 是挂在 Function 对象上的三个方法,调用这三个方法的必须是一个函数。

1
2
3
4
// 基本语法:
func.call(thisArg, param1, param2, ...)
func.apply(thisArg, [param1,param2,...])
func.bind(thisArg, param1, param2, ...)

其中 func 是要调用的函数,thisArg 一般为 this 所指向的对象,后面的 param1、2 为函数 func 的多个参数,如果 func 不需要参数,则后面的 param1、2 可以不写。
这三个方法共有的、比较明显的作用就是,都可以改变函数 func 的 this 指向。call 和 apply 的区别在于,传参的写法不同:apply 的第 2 个参数为数组; call 则是从第 2 个至第 N 个都是给 func 的传参;而 bind 和这两个(call、apply)又不同,bind 虽然改变了 func 的 this 指向,但不是马上执行,而这两个(call、apply)是在改变了函数的 this 指向之后立马执行。

下面例子分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let a = {
name: 'jack',
getName: function(msg) {
return msg + this.name;
}
}
let b = {
name: 'lily'
}
console.log(a.getName('hello~')); // hello~jack
console.log(a.getName.call(b, 'hi~')); // hi~lily
console.log(a.getName.apply(b, ['hi~'])) // hi~lily
let name = a.getName.bind(b, 'hello~');
console.log(name()); // hello~lily

A 对象有个 getName 的方法,B 对象也需要临时使用同样的方法,那么这时候我们是单独为 B 对象扩展一个方法,还是借用一下 A 对象的方法呢?当然是可以借用 A 对象的 getName 方法,既达到了目的,又节省重复定义,节约内存空间。

new的实现

执行 new 的过程

  • 让实例可以访问到私有属性
  • 让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
  • 构造函数返回的最后结果是引用数据类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function _new(ctor, ...args) {
    if(typeof ctor !== 'function') {
    throw 'ctor must be a function';
    }
    let obj = new Object();
    obj.__proto__ = Object.create(ctor.prototype);
    let res = ctor.apply(obj, [...args]);

    let isObject = typeof res === 'object' && typeof res !== null;
    let isFunction = typeof res === 'function';
    return isObect || isFunction ? res : obj;
    };

    call的实现

    1
    2
    3
    4
    5
    6
    7
    Function.prototype.call = function (context, ...args) {
    var context = context || window;
    context.fn = this;
    var result = eval('context.fn(...args)');
    delete context.fn
    return result;
    }

    apply的实现

    1
    2
    3
    4
    5
    6
    7
    Function.prototype.call = function (context, ...args) {
    var context = context || window;
    context.fn = this;
    var result = eval('context.fn(...args)');
    delete context.fn
    return result;
    }
    实现 call 和 apply 的关键就在 eval 这行代码。其中显示了用 context 这个临时变量来指定上下文,然后还是通过执行 eval 来执行 context.fn 这个函数,最后返回 result。

    bind的实现

    bind 的实现思路基本和 apply 一样,但是在最后实现返回结果这里,bind 和 apply 有着比较大的差异,bind 不需要直接执行,因此不再需要用 eval ,而是需要通过返回一个函数的方式将结果返回,之后再通过执行这个结果,得到想要的执行效果。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Function.prototype.bind = function (context, ...args) {
    if (typeof this !== "function") {
    throw new Error("this must be a function");
    }
    var self = this;
    var fbound = function () {
    self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
    }
    if(this.prototype) {
    fbound.prototype = Object.create(this.prototype);
    }
    return fbound;
    }
    从上面的代码中可以看到,实现 bind 的核心在于返回的时候需要返回一个函数,故这里的 fbound 需要返回,但是在返回的过程中原型链对象上的属性不能丢失。因此这里需要用Object.create 方法,将 this.prototype 上面的属性挂到 fbound 的原型上面,最后再返回 fbound。这样调用 bind 方法接收到函数的对象,再通过执行接收的函数,即可得到想要的结果。

闭包

什么是闭包?

红宝书闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。
MDN:一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

闭包基本概念

通俗地讲解一下:闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以。(函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。)
因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。下面我们通过代码先来看一个简单的例子。

1
2
3
4
5
6
7
8
9
function fun1() {
var a = 1;
return function(){
console.log(a);
};
}
fun1();
var result = fun1();
result(); // 1

闭包的表现形式

  • 返回一个函数
  • 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。请看下面这段代码,这些都是平常开发中用到的形式。
    1
    2
    3
    4
    5
    6
    7
    8
    // 定时器
    setTimeout(function handler(){
    console.log('1');
    },1000);
    // 事件监听
    $('#app').click(function(){
    console.log('Event Listener');
    });
  • 作为函数参数传递的形式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var a = 1;
    function foo(){
    var a = 2;
    function baz(){
    console.log(a);
    }
    bar(baz);
    }
    function bar(fn){
    // 这就是闭包
    fn();
    }
    foo(); // 输出2,而不是1
  • IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量
    1
    2
    3
    4
    var a = 2;
    (function IIFE(){
    console.log(a); // 输出2
    })();

循环中使用闭包解决 var 定义函数的问题

1
2
3
4
5
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}

从控制台执行的结果可以看出来,结果输出的是 5 个 6,那么为什么都是 6 ?

1.setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
2.因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

如何实现输出1,2,3,4,5?
  • 利用 IIFE(立即执行函数)
    1
    2
    3
    4
    5
    6
    7
    for(var i = 1;i <= 5;i++){
    (function(j){
    setTimeout(function timer(){
    console.log(j)
    }, 0)
    })(i)
    }
    当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。
  • 使用 ES6 中的 let
    1
    2
    3
    4
    5
    for(let i = 1; i <= 5; i++){
    setTimeout(function() {
    console.log(i);
    },0)
    }
    ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行
  • 定时器传入第三个参数
    1
    2
    3
    4
    5
    for(var i=1;i<=5;i++){
    setTimeout(function(j) {
    console.log(j)
    }, 0, i)
    }
    setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。那么结合第三个参数,调整完之后的代码如下。
    1
    2
    3
    4
    5
    for(var i=1;i<=5;i++){
    setTimeout(function(j) {
    console.log(j)
    }, 0, i)
    }
    从中可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现我们想要的结果,这也是一种解决循环输出问题的途径。

JSON.stringify

—————— 该篇文章继续学习中,未完待续 ——————

--------- 本文结束感谢您的阅读 ---------
分享