咱也可以看看咱之前写的JavaScript进阶1
1. JS的数据类型你了解多少?
1.1 概念
1.1.1 数据类型有以下八种
undefined
Null
Boolean
String
Number
Symbol
BigInt
Object
Array
RegExp
Date
Math
Function
1.1.2 数据类型的存储
- 基础数据类型存储在栈内存中,被引用或拷贝时,会创建一个完全相等的变量
- 引用类型会存储在堆内存中,存储的是地址,多个引用指向同一个地址,这里会涉及"共享"的概念
什么是共享?看下面两道例题
let a = {
name: 'lee',
age: 18
}
let b = a;
console.log(a.name);
b.name = 'son';
console.log(a.name);
console.log(b.name);
这里打印
lee,son,son
我们可以发现
b
改变了name
,a
的name
也跟着改变了,这是因为他们都是引用数据类型,他们存储的都是堆内存中地址,指向的是同一个数据
let c = {
name:'Julia',
age: 20
}
function change(o) {
o.age = 24;
o = {
name: 'Kath',
age: 30
}
return o;
}
let d = change(c);
console.log(d.age, d.name);
console.log(c.age, c.name);
这里打印
30 Kath, 24 Julia
change
函数中return o
; 相当于是改变了内存的地址,即新的一个地址,所以d.age
指向的是o
而
c.age
是因为最开始传入的c
, 和change
函数中第一行代码o
共享一个堆内存的数据,指向的是同一个地址,所以有一个改变了,另一个也会变。
总结
- 所有引用数据类型存储的都是指向堆内存的地址
function
内直接改变,那么所有指向这个地址的数据都会跟着改变return
一个新的数据,那么会建立一个新的内存地址去存储
1.2 数据检测
1.2.1 typeof
- 优点:
typeof
操作符是一种简单、快速的方式来判断基本数据类型。它返回的结果是一个字符串,可以直接用于条件判断。 - 缺点:对于引用类型(除了function)的判断结果都是’object’,无法细分具体的引用类型。同时,对于
null
的判断结果也是object
,不能准确判断null
。
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof undefined); // undefined
console.log(typeof true); // boolean
console.log(typeof Symbol()); //symbol
console.log(typeof null); // object
console.log(typeof []); // obejct
console.log(typeof {}); // object
console.log(typeof console); // object
console.log(typeof console.log); // function
可以看到前6个都是基本数据类型,但是第6个
null
却是object
, 这是js
的历史遗留问题,我们可以直接用===
来判断是否为null
1.2.2 instanceof(附手撕实现)
instanceof
是JavaScript
的一个操作符,用于检查某个对象是否是某个特定类的实例。
- 优点:
instanceof
操作符可以判断一个对象是否是某个构造函数的实例 ,可以用于自定义构造函数的判断。它可以处理继承关系,如果对象是某个构造函数的子类实例,也会返回true
。 - 缺点:
instanceof
操作符只能判断对象是否是特定构造函数的实例,不能判断基本数据类型的数据。此外,如果在多个窗口或框架中使用,可能会导致不准确的结果。
示例
let Car = function(){};
let benz = new Car();
console.log(benz instanceof Car); // true
let car = new String('Mecredes Benz');
console.log(car instanceof Car); // false
let str = 'Covid-19';
console.log(str instanceof String); // false
// 这里说明不能判断基本数据类型,只能判断是否是构造函数的实例
手写实现
instanceof
// 手撕instanceof
function myInstanceof(left, right) {
// 这里用typeof来判断是否是基础数据类型,如果是,直接返回false,注意null要单独处理
if (typeof left !== 'object' || left === null) return false;
//getPrototypeOf 是 Obejct对象带的API,可以拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
// 循环往下找,直到找到相同的原型对象
while (true) {
if (proto === null) return false; // 没找到
if (proto === right.prototype) return true; // 找到相同的原型对象
proto = Object.getPrototypeOf(proto);
}
}
// 测试 myInstanceof
console.log('--------myInstanceof')
let testInstanceof = function (){};
testIns1 = new testInstanceof();
console.log(myInstanceof(testIns1, testInstanceof));
let testIns2 = new String('手撕Instanceof')
console.log(myInstanceof(testIns2, testInstanceof));
let testIns3 = 'asd';
console.log(myInstanceof(testIns3, String));
1.2.3 Object.prototype.toString.call
每一个继承
Object
的对象都有toString
方法,如果toString
方法没有重写的话,会返回[Object type]
,其中type
为对象的类型。但当除了Object
类型的对象外,其他类型直接使用toString
方法时,会直接返回都是内容的字符串,所以我们需要使用call
或者apply
方法来改变toString
方法的执行上下文(不懂执行上下文可以点击看看这篇文章)。
示例
/*------ Object.prototype.toString -------*/
console.log('----------Object.prototype.toString');
console.log(Object.prototype.toString.call({}));
console.log(Object.prototype.toString.call(1));
console.log(Object.prototype.toString.call('1'));
console.log(Object.prototype.toString.call(true));
console.log(Object.prototype.toString.call(function(){}));
console.log(Object.prototype.toString.call(null));
console.log(Object.prototype.toString.call(undefined));
console.log(Object.prototype.toString.call(Symbol));
console.log(Object.prototype.toString.call(new Date()));
console.log(Object.prototype.toString.call(/123/g));
console.log(Object.prototype.toString.call([]));
console.log(Object.prototype.toString.call(document));
console.log(Object.prototype.toString.call(window));
console.log(Object.prototype.toString.call(NaN));
这个方法就很强大了,都可以判断。我们如果想就拿到后面的结果,可以用
slice
截取吧就像这样Object.prototype.toString.call(1).slice(7,-1)
1.3 数据类型转换
1.3.1 强制类型转换
首先咱先看看强制类型转换的方法都有啥子 (这里就前面两个面试喜欢问,当然还有一些比如字符串的加法规则啥的也有)
-
Number()
-
布尔值,则返回1或0
-
数字返回自身
-
null
返回0 -
undefined
返回NaN
-
字符串
- 字符串只包含数字,则转换为十进制
- 字符串包含有效格式的浮点数,将其转化为浮点数值
- 空字符串转化为0
- 其它情况均返回
NaN
-
Symbol
抛出错误 -
object
并且部署了[Symbol.toPrimitive]
方法,则会调用此方法,否则会调用对象的valueOf()
方法Symbol.toPrimitive
: 指将被调用的指定函数值的属性转换为相对应的原始值
-
测试
/*----------- 强制类型转换 -----------*/ // Number() console.log('---Numebr()'); console.log(Number(true)); // 1 console.log(Number(false)); // 0 console.log(Number('0111')); // 111 console.log(Number(null)); // 0 console.log(Number('')); // 0 console.log(Number('la')); // NaN console.log(Number(-0X11)); // -17 console.log(Number('0X11')); // 17 console.log(Number('-0X11')); // NaN
-
-
Boolean()
undefined
、null
、false
、''
、0(包括+0和-0)
、NaN
转换出来都是false
- 其他的都是
true
- 测试
console.log('----Boolean()'); console.log(Boolean(0)); // false console.log(Boolean(null)); // false console.log(Boolean(undefined)); // false console.log(Boolean(NaN));// false console.log(Boolean(1)); // true console.log(Boolean(13));// true console.log(Boolean('12'));// true
-
parseInt()
- 解析一个字符串,并返回一个整数
parseInt(String, radix)
其中第一个参数是待转换的字符串,第二个参数是要转换的进制,如果进制在2--36
之外,则会返回NaN
- 如果第一个参数为
null
或者为空字符串则会NaN
- 如果字符串不是表示
Number
类型的值,也会NaN
- 测试
// parseInt() console.log('--------parseInt()'); console.log(parseInt('0', 10)); // 0 console.log(parseInt('-0', 10)); // -0 console.log(parseInt('470', 10)); // 470 console.log(parseInt('-FF', 16)); // -255 console.log(parseInt('1100110', 2));// 102 console.log(parseInt('Kona', 27)); // 411787 console.log(parseInt('123', 1)); // NaN console.log(parseInt('', 10)); // NaN
-
parseFloat()
parseFloat
与parseInt
的区别就是它是把字符串转化为浮点数,其他的规则一样。
-
toString()
(深入剖析)- 这个看名字应该就知道是转化为
String
吧,第二次碰到了,还是写下来吧,本来在第二小节用Object.prototype.toString.call
来判断数据类型就应该写的 - 每个对象都有一个
toString()
方法,我们可以通过hasOwnProperty()
方法来判断toString
是不是该对象的自带属性。当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()
方法被每个Object
对象继承。如果此方法在自定义对象中未被覆盖,toString()
返回[object type]
,其中type
是对象的类型。 - 不同类型对象的
toString()
方法的返回值不同,因为他们都自己重写过,请看下面代码来证实
// 剖析toString() console.log('-------剖析toString()') // 首先看看这些数据类型,toString是不是他们自身属性 console.log(Number.prototype.hasOwnProperty('toString')); // 全为true console.log(Object.prototype.hasOwnProperty('toString')); console.log(String.prototype.hasOwnProperty('toString')); console.log(Array.prototype.hasOwnProperty('toString')); console.log(Date.prototype.hasOwnProperty('toString')); console.log(RegExp.prototype.hasOwnProperty('toString')); console.log(Function.prototype.hasOwnProperty('toString')); // 然后再验证不同的构造函数生成的对象,其toString返回值不同 console.log('然后再验证不同的构造函数生成的对象,其toString返回值不同'); let num = new Number('123asd'); console.log(num.toString()); // NaN let str = new String('123asd'); console.log(str.toString()); // '123asd' let bool = new Boolean('123asd'); console.log(bool.toString()); // true let arr = new Array(1,2); console.log(arr.toString()); // '1,2' let date = new Date(); console.log(date.toString()); // xxx时间 let fn = function() {}; console.log(fn.toString()); // function(){} // 下面两个就会直接返回对象类型,即没有封装自己toString let obj = new Object({}); console.log(obj.toString()); // [obejct Object] console.log(Math.toString()); // [object Math]
- 所以之前用
Object.prototype.toString
就要加上.call()
方法改变他的执行上下文,此外,我们直接对那些封装了自己的toString
方法的数据类型,譬如Number.toString()
会输出一个函数
- 这个看名字应该就知道是转化为
-
String()
- 至于这个方法很明显就是把其它类型转化为
String
类型,没有什么要注意的,不过对于对象,是返回[object Obeject]
,测试如下
// String() console.log('----String()') console.log(String(null)); // null console.log(String(undefined)); // undefined console.log(String(true)); // true console.log(String(false)); // false console.log(String(Symbol)); // function Symbol() { [native code] } console.log(String(Object)); // function Object() { [native code] } console.log(String([1,2,3])); // 1,2,3 console.log(String({id:1,name:2,age:3})); // [object Object] console.log(String(1)); // 1 console.log(String('1')); // 1
- 至于这个方法很明显就是把其它类型转化为
1.3.2 隐式类型转换
==
隐式类型转换规则- 如果类型相同,则无需进行类型转换
- 如果其中一个操作值为
null
或undefined
,那么另一个操作符必须为null
或undefined
才为true
, 否则为false
- 如果其中一个为
Symbol
类型, 则返回false
- 如果两个操作值一个为
String
,一个为Number
, 则会转化为Number
- 如果一个操作值是
boolean
则会转化为Number
- 如果一个操作值为
Object
, 且另一个为String、Number、Symbol
就会把Object
转化为原始类型再判断
+
隐式类型转换规则- 如果一个为
Number
, 一个为String
,则会把Number
转化为String
,再进行字符串拼接 - 如果一个是
String
, 另外一个是undefined 、 null 、 Boolean
,则调用toString
方法进行字符串拼接 - 如果一个是
Number
,另一个是undefined 、 null 、 Boolean
,则会转化为数字再进行加法运算
- 如果一个为
/*----------- 隐式类型转换 ------------*/
console.log('-------隐式类型转换');
console.log('abc' + 123 + 5); // abc1235
console.log(123 + 5 + 'abc'); // 128abc
console.log(undefined + '111'); // undefined111
console.log('111' + null); // 111null
console.log('abc' + Boolean); //abcfunction Boolean() { [native code] }
console.log(123 + undefined); // NaN
console.log(null + 123); // 123
console.log(true + 123); // 124
console.log(123 + Boolean); //123function Boolean() { [native code] }
2. 浅拷贝与深拷贝
2.1 浅拷贝
自己创建一个新的对象,来接受你要重新赋值或引用的对象值。如果对象属性是基本数据类型,复制的就是基本数据类型的值给新对象;但如果是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存的地址,就会影响到其它同样指向这个地址的对象。
简单地说,浅拷贝就是,当是基本数据类型时候,就是复制了他的值
但是如果是引用数据类型的时候,复制的是内存地址,也就是
1.1.2 数据类型的存储
中的共享概念但是它影响只会影响最表层那一块。譬如下面这个例子
let obj = {a:1, b: {c: 1}};
let obj2 = {...obj};
console.log('obj', obj); // 打印obj {a:2, b: {c: 2}}
console.log('obj2', obj2); // 打印obj2 {a:1, b: {c: 2}};
obj.a = 2;
obj.b.c = 2;
2.1.1 object.assign
es6的新方法,可以用于多个对象的合并,也就可以用来进行浅拷贝
- 它不会拷贝对象的继承属性
- 不会拷贝对象的不可枚举属性
- 可以拷贝
Symbol
类型的属性
示例一
let target = {};
let source = {a: {b: 1}};
Object.assign(target, source);
console.log(target); // {a: {b: 10}}
source.a.b = 10;
console.log(source); // {a: {b: 10}}
console.log(target); // {a: {b: 10}}
示例二,测试不能拷贝不可枚举属性和可以拷贝
Symbol
类型属性这里不知道
Object.defineProperty
的可以看看这篇文章Object.defineProperty 详解 , Vue响应式就是通过这个方法来实现的
let obj1 = {a: {b: 1}, sym: Symbol(1)};
Object.defineProperty(obj1, 'name', {
value: '不可枚举属性',
enumerable: false, // 设置不能遍历
});
let obj2 = {};
Object.assign(obj2, obj1);
obj1.a.b = 2;
console.log('obj1', obj1);
console.log('obj2', obj2); // name属性没有拷贝过来,因为设置了enumerable: false
2.1.2 …扩展运算符进行浅拷贝
console.log('---扩展运算符')
let obj3 = {a:1, b: {c: 1}};
let obj4 = {...obj3};
obj3.a = 2;
console.log(obj3);
obj3.b.c = 2;
console.log(obj4);
let arr1 = [1,2,3];
let newArr1 = [...arr1];
console.log(newArr1);
2.1.3 手撕浅拷贝
function shallowClone(target) {
// 首先判断是否为引用类型,如果不是的话则直接返回
if(typeof target === 'object' && target !== null) {
// 判断是数组还是对象,进行初始化定义
let cloneTarget = Array.isArray(target)? [] : {};
for(let prop in target) {
// 只复制表层的属性值,即target自带的属性值
if(target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
}else {
// 不是引用类型直接返回
return target;
}
}
测试手写的浅拷贝
let obj = {a:1, b: {c: 1}};
let obj2 = {};
obj2 = shallowClone(obj);
console.log('obj', obj); // 打印obj {a:2, b: {c: 2}}
console.log('obj2', obj2); // 打印obj2 {a:1, b: {c: 2}}
obj.a = 2;
obj.b.c = 2;
2.2 深拷贝
对于复杂引用类型,其在堆内存中完全开辟一块新的内存地址,将原有对象完整的复制过来,修改并不会对原有对象造成影响。
2.2.1 JSON.parse(JSON.stringify())
最简单的深拷贝方法就是
JSON.parse(JSON.stringify())
原理就是先用
JSON.stringify
将其转换为单纯地JSON
对象,而后再将其转化为JS
的对象,这样就会新创建一个堆内存地址存放新的JS
对象。
使用
JSON.parse(JSON.stringify())
进行深拷贝的注意事项
- 拷贝对象的值如果有
function
、undefined
、symbol
这几种类型,经过JSON.stringify
序列化后的字符串中,这个键值对会消失 - 拷贝
Date
引用类型会变成字符串 - 无法拷贝不可枚举的属性
- 无法拷贝对象的原型链
- 拷贝
RegExp
引用类型会变成空对象 - 对象中含有
NaN
、±Infinity
,JSON
序列化结果会变成null
- 无法拷贝对象的循环应用,即对象成环现象(
obj[key] = obj
)
测试
/*------ JSON.parse(JSON.stringify()) --------*/
let obj1 = {a:1, b: [1,2,3], c: undefined, d: () => {console.log('d')}, e: Symbol(1)};
Object.defineProperty(obj1, 'name', {
value: '不可枚举属性',
enumerable: false
})
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log('obj2',obj2);
obj1.a = 2;
obj1.b.push(4);
console.log('obj1', obj1);
console.log('obj2', obj2);
// 打印结果如下图
2.2.2 手撕深拷贝(简陋版)
/*---------- 手撕深拷贝V1.0 -----------*/
function deepClone1(obj) {
let cloneObj = {};
for(let key in obj) {
// 如果是引用数据类型,则递归调用
if(typeof obj[key] === 'object' && obj[key] !== null) {
cloneObj[key] = deepClone1(obj[key])
}else {
cloneObj[key] = obj[key];
}
}
return cloneObj;
}
// 测试
let obj1 = {
a: {
b: 1,
},
b: 1,
c: undefined,
reg: /123/,
NaN: NaN,
infinity: Infinity,
sym: Symbol(1),
Null: null,
};
Object.defineProperty(obj1, 'name', {
value: 'aaa',
enumerable: false
})
let obj2 = deepClone1(obj1);
obj1.a.b = 2;
obj1.b = 2;
console.log(obj1,'obj1');
console.log(obj2,'obj2');
//打印结果如下
我们可以发现
RegExp
对象还是没有拷贝过来,除此之外,name
属性不可枚举也没有拷贝过来
2.2.3 手撕深拷贝(改进版)
针对简陋版的缺陷我们可以用以下方法解决
- 针对能够遍历对象的不可枚举属性,我们可以用
Reflect.ownKeys()
方法获取一个由目标对象自身的属性键组成的数组。Reflect.ownKeys(target)
的返回值等于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
- 当参数为
RegExp
或Date
类型时,则直接生成一个新的实例返回 - 用
Object.getOwnPropertyDescriptors()
获得对象的所有属性,以及对应的特性,结合Object.create()
创建一个新对象,并继承传入原对象的原型链 - 用
WeakMap
类型作为Hash
表, 因为WeakMap
是弱引用类型 ,可以有效防止内存泄漏,作为检测循环引用很有帮助,如果存在循环,则引用直接返回WeakMap
存储的值,并且不影响垃圾回收机制,避免造成内存泄漏,提高性能。
手撕深拷贝
// 判断是否为复杂引用类型
const isComplexDataType = obj => (typeof obj === 'object' && obj !== null);
const deepClone = (obj, hash = new WeakMap()) => {
// 如果是Date或RegExp类型直接返回新对象
if(obj.constructor === Date) {
return new Date(obj);
}
if(obj.constructor === RegExp) {
return new RegExp(obj);
}
// 如果存在循环引用,即存在obj[key] = obj;用WeakMap解决
if(hash.has(obj)) {
return hash.get(obj);
}
let allDesc = Object.getOwnPropertyDescriptors(obj); // 获取obj自身的所有属性及其特性
// 创建新对象cloneObj,并继承obj自身的原型,并传入obj自身的所有属性和属性的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
// 递归继承其原型链
hash.set(obj, cloneObj); // 以键为obj,值为cloneObj,通过递归不断更新
for(let key of Reflect.ownKeys(obj)) {
// 如果是复杂引用类型则递归调用
cloneObj[key] = isComplexDataType(obj[key]) ? deepClone(obj[key], hash) : obj[key];
}
return cloneObj;
}
测试
// 验证深拷贝V2.0
console.log('----手撕深拷贝V2.0')
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '一个对象', value: 1 },
arr: [0, 1, 2],
fn: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1
};
// 加入aaa属性,设置为不可枚举
Object.defineProperty(obj, 'aaa', {
value: '不可枚举属性',
enumerable: false
});
// 设置循环引用属性loop
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj));
obj.loop = obj;
// 测试
let cloneObj = deepClone(obj);
cloneObj.arr.push(4);
console.log('obj', obj);
console.log('cloneObj', cloneObj);
3. JS的继承你了解多少?
不妨先来看两个面试题
JavaScript
的继承有多少种方式实现?我目前只想到了
call/apply/bind
,然后深拷贝,然后还可以用Object.create
实现继承吧
ES6
的extends
关键字是用哪种继承方式实现的?不知道啊啊
等咱们学完了再来看这两个问题吧啊
3.1 概念
继承主要是用构造函数以及原型链实现继承效果。它可以让子类对象(实例)使用父类的所有属性以及方法,并且可以直接在子类上扩展新的属性或方法。
使用继承可以提高我们代码的复用性,从而减少我们的代码量,降低开发成本。
3.2 7种继承方法
3.2.1 原型链继承
- 每一个构造函数都有一个原型对象
- 原型对象又包含一个指向构造函数的指针
- 而实例则包含一个原型对象的指针
/*--------- 1. 原型链继承 ---------*/
function Parent1() {
this.name = 'parent1';
this.play = [1, 2, 3];
}
function Child1() {
this.type = 'child1';
}
Child1.prototype = new Parent1();
Child1.prototype.constructor = Child1;
console.log(new Child1());
// 缺点 就是共享问题,指向同一个堆内存地址,一个变其它都会变
let child1 = new Child1();
let child2 = new Child1();
child1.play.push(4);
console.log('child1', child1); // 数组全部变成了[1, 2, 3, 4];
console.log('child2', child2);
3.2.2 构造函数继承
他是父类的引用属性,不会被共享,解决了第一种方法的弊端
但是只能继承实例的属性和方法,不能继承原型的属性和方法
/*----------- 2. 构造函数继承 ---------*/
function Parent2() {
this.name = 'parent2';
}
Parent2.prototype.getName = function() {
return this.name;
}
function Child2() {
Parent2.call(this);
this.type = 'child2';
}
let child2 = new Child2();
console.log('child2', child2);
console.log(child2.getName()); // 报错
3.2.3 组合继承
结合前两种继承方法的优点,就有了这种继承方法
这种方法解决了上面两种方法的缺陷
但是调用了两次
Parent3
,进行两次构造,一次是指定child3.prototype
时候, 还一次是通过call
调用改变this
指向的时候, 多构造一次就多进行了一次性能开销,还有更好的办法吗?
console.log('-------3. 组合继承')
function Parent3() {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function() {
return this.name;
}
function Child3() {
// 第二次调用Parent3();
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用Parent3
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己构造函数
Child3.prototype.constructor = Child3;
// 测试组合继承
let child3_1 = new Child3();
let child3_2 = new Child3();
child3_1.play.push(4);
console.log(child3_1.play, child3_2.play); // 不互相影响
console.log(child3_1.getName()); // 正常使用原型上的函数
3.2.4 Object.create 原型式继承
嘿,我就说她也可以吧
/*----------- 4. 原型式继承 ---------*/
console.log('-------4. 原型式继承')
let Parent4 = {
name: 'parent4',
play: [1, 2, 3],
getName: function() {
return this.name;
}
}
let child4_1 = Object.create(Parent4);
let child4_2 = Object.create(Parent4);
child4_1.name = 'child4_1';
child4_1.play.push(4);
child4_2.name = 'child4_2';
child4_2.play.unshift(4);
console.log('child4_1', child4_1);
console.log('child4_2', child4_2);
console.log(child4_1.getName());
看打印可以发现,多个实例引用指向相同的内存,存在篡改的可能。
3.2.5 寄生式继承
在原型式继承的基础上进行优化
使用原型是继承获取一份目标对象的浅拷贝,然后利用这个浅拷贝在父类上增加一些方法。
/*----------- 5. 寄生式继承 ---------*/
console.log('-------5. 寄生式继承')
let Parent5 = {
name: 'parent5',
play: [1, 2, 3],
getName: function() {
return this.name;
}
}
function clone(original) {
let clone = Object.create(original);
clone.getPlay = function() {
return this.play;
}
return clone;
}
// 测试
let child5 = clone(Parent5);
console.log(child5.getName());
console.log(child5.getPlay());
3.2.6 寄生组合式继承
结合前面的方法的优缺点得出这种继承方法,这也是目前最优的继承方式
/*----------- 6. 寄生组合式继承 ---------*/
console.log('--------6. 寄生组合式继承')
function clone2(parent, child) {
// 这里改用Object.create 就可以减少组合继承中多进行一次Parent3构造的过程
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);
type:'child6';
this.friends = 'child6';
}
clone2(Parent6, Child6); // 继承
Child6.prototype.getFriends = function() {
return this.friends;
}
// 测试
let child6_1 = new Child6();
let child6_2 = new Child6();
child6_1.play.push(4);
child6_2.play.unshift(4);
console.log('child6_1', child6_1);
console.log('child6_2', child6_2);
console.log(child6_1.getName());
console.log(child6_1.getFriends());
3.2.7 ES6的extends关键字实现继承
/*----------- 7. extends实现继承 ---------*/
console.log('------7. extends实现继承')
class Person {
constructor(name) {
this.name = name;
}
getName = function() {
console.log('Person: ', this.name);
}
play = [1, 2, 3];
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用 this 之前先调用 super();
super(name);
this.age = age;
}
}
let a = new Gamer('A', 20);
let b = new Gamer('B', 22);
a.getName();
a.play.push(4);
b.play.pop();
console.log('a', a);
console.log('b', b);
这里需要注意,当浏览器不兼容
ES6
时候我们要做向下兼容,比如IE8/9
就不支持。可以用babel
继承记忆图
我觉得还可以看看这个深入理解JavaScript——继承
3.3 手撕New、 apply & bind &call
不妨先思考下面三个问题
- 怎么实现
new
关键字?apply、call、bind
三者有什么区别?- 怎么实现
apply
或者call
方法
3.3.1 new 原理介绍
new
也被称为隐式原型继承,这个关键字的主要作用如下
- 在内存中创建一个新对象
- 将新对象的
[[Prototype]]
即__proto__
被赋值为构造函数的prototype
属性- 将构造函数中的
this
指向新对象- 执行构造函数中的代码
- 如果构造函数返回非空对象,则返回该对象;否则返回创建的新对象。
我们先来看看一个常见的
new
的例子
function Person() {
this.name = 'Jack';
}
let p = new Person();
console.log(p.name);
需要注意的是第一条,当复杂引用类型返回的是一个非空对象,那么
new
返回的就是那个非空对象,而不会执行构造函数里的代码,如果返回的不是对象,那么还是执行构造函数里的代码,也就是接收的是新创建的对象。比如说说当
Person()
里面返回一个return {age: 30}
; 那么p.name
就是undefined
了
3.3.2 手撕new(2种方法)
回顾一下
new
被调用后大致做了哪几件事情?
- 在内存中创建一个新对象
- 将新对象的
[[Prototype]]
即__proto__
被赋值为构造函数的prototype
属性- 将构造函数中的
this
指向新对象- 执行构造函数中的代码
- 如果构造函数返回非空对象,则返回该对象;否则返回创建的新对象。
/*----- new原理 -------*/
// 简单的new使用例子
function Person() {
this.name = 'Jack';
}
let p = new Person();
console.log(p.name);
/*----- 手撕new ------*/
function _new(Constructor, ...args) {
if (typeof Constructor !== 'function') {
throw 'Constructor must be a function';
}
// 1. 创建一个新对象
let obj = Object.create(null);
// 2. 将新对象的 `[[Prototype]]` 特性被赋值为构造函数的 `prototype` 属性
obj.__proto__ = Constructor.prototype;
// 3. 构造函数内部的 `this` 被赋值给这个新对象
// 4. 执行构造函数内部的代码
let result = Constructor.apply(obj, args);
// 5. 如果构造函数返回非空对象, 则返回该对象, 否则返回新对象
return (typeof result === 'object' || typeof result === 'function') ? result : obj;
}
// 测试
console.log('测试手写的new');
let p2 = _new(Person,);
console.log(p2.name);
/*--------- 手撕new2 -----------*/
// ES5之前没有 Object.create,要怎么实现new?
function _new2() {
// 基于new Object 创建实例。
let obj = new Object();
// 获取外部的构造器
Constructor = Array.prototype.shift.call(arguments);
// 手写Object.create()的核心
let F = function () { };
F.prototype = Constructor.prototype;
// 指向正确的原型
obj = new F();
// 借用外部传入的构造函数给obj设置属性
let result = Constructor.apply(obj, arguments);
//执行结果如果返回非空对象, 则返回该对象, 否则返回新对象
return (typeof result === 'object' || typeof result === 'function') ? result : obj;
}
console.log('测试不用Object.create写出来的new')
let p3 = _new2(Person);
console.log(p3.name);
3.3.3 apply & call & bind 原理介绍
call、apply、bind
是挂在Function
对象上的三个方法,调用这三个方法必须是一个函数, 他们都可以改变函数的this
指向其中
call、apply
的区别是传参写法不同,call
是从第2
个到第n
个就是在传参,而apply
第二个是一个数组,参数放数组里面而
bind
与前两者的区别就是,虽然它也改变this
指向,但是它不是立即执行的,而前两者是立即执行的。
他们的基本语法如下
func.call(thisArg, param1, param2, ...);
func.apply(thisArg, [param1, param2, ...]);
func.bind(thisArg, param1, param2, ...);
使用示例
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
3.3.4 手撕 apply & call & bind
我们先来想想
apply 和 call
的注意点
- 改变
this
指向- 直接在
Function
原型上调用得到undefined
,比如Function.prototype.call(xx)
得到的是undefined
,必须要有实例。- 考虑
call
为null
的情况,那么那时候this
指向window
了apply
与call
的区别就是参数传递方式的不同,他们都是返回函数执行得到的结果,而bind
返回的是待执行的函数。所以在call和apply
里我们是首先改变函数的this
指向,然后得到函数执行的结果, 并返回执行结果
手撕
apply & call
Function.prototype.myCall = function(context, ...args) {
if(this === Function.prototype) {
return undefined; // 防止Function.prototype.mycall 直接调用
// 譬如Function.prototype.call(a); // undefined
}
var context = context || window; // 考虑fn.call(null)的情况,如果是null则指向window,防御性代码
context.fn = this; // 改变函数的this指向
let result = context.fn(...args) // 立即执行函数并得到结果
delete context.fn // 删除context属性,释放内存,并返回结果result
return result;
}
Function.prototype.myApply = function(context = window, args) {
if(this === Function.prototype) {
return undefined;
}
context.fn = this;
let result = context.fn(...args);
delete context.fn;
return result;
}
// 测试手撕
console.log('---测试myCall---');
console.log(a.getName.myCall(b, 'hi~'));
console.log('----测试myApply-----');
console.log(a.getName.myApply(b, ['hello~']));
手撕
bind
- 首先看到
bind
是返回一个函数,并且返回的函数可以接受参数,说明它是一个闭包。- 其次,
bind
不能原型方法调用,否则就会报错,所以我们根据这个特性,分为两种情况,如果this instanceof Fn
为true
, 那么说明它是构造函数调用,如果是,我们就要用new
创建一个新对象来调用函数,如果不是则apply
出来context
Function.prototype.myBind = function(context, ...args1) {
if(this === Function.prototype) {
throw new TypeError('Error');
}
const _this = this;
return function F(...args2) {
if(this instanceof F) { // 判断是否为构造函数
// 如果是构造函数,则用最初的fn为构造器调用函数
return new _this(...args1, ...args2);
}
// 如果不是构造函数,则用apply指向代码
return _this.apply(context, args1.concat(args2));
}
}
console.log('----测试myBind----');
let myBindName = a.getName.myBind(b, 'hello~');
console.log(myBindName());
4. this
4.1 this的4种绑定方式
4.1.1 默认绑定
直接在全局范围内调用函数,
this
指向Windows
全局
console.log('----默认绑定')
function girl() {
console.log(this);
}
girl();
// 打印window对象
4.1.2 隐式绑定
调用一个对象的方法时,会出现隐式绑定,
this
此时就会指向方法
console.log('----隐式绑定')
let girl2 = {
name: '小红',
height: 160,
weight: 110,
detail: function () {
console.log('姓名:' + this.name);
console.log('身高:' + this.height);
console.log('体重:' + this.weight);
}
}
girl2.detail();
/*打印
----隐式绑定
姓名:小红
身高:160
体重:110
*/
4.1.3 硬绑定
使用
call、apply
方法改变this
指向
/*--------------- 硬绑定 ---------------*/
// 使用call、apply方法改变this指向
console.log('----硬绑定')
let girlName = {
name: '小红',
sayName: function () {
console.log('我的女朋友是' + this.name);
}
}
let girlName1 = {
name: '小白'
}
let girlName2 = {
name: '小黄'
}
console.log('----call')
girlName.sayName.call(girlName1);
girlName.sayName.call(girlName2);
console.log('----apply')
girlName.sayName.apply(girlName1);
girlName.sayName.apply(girlName2);
/* 打印
----硬绑定
----call
我的女朋友是小白
我的女朋友是小黄
----apply
我的女朋友是小白
我的女朋友是小黄
*/
4.1.4 构造函数绑定
console.log('----构造函数绑定')
function Lover(name) {
this.name = name;
this.sayName = function() {
console.log('我的老婆是' + this.name);
}
}
let name = '小白';
let xiaoHong = new Lover('小红');
xiaoHong.sayName();
/* 打印
----构造函数绑定
我的老婆是小红
*/
4.2 this指向相关3道练习题
先想想这三道题分别打印什么,答案在后面
4.2.1 第一道
function x() {
function y() {
console.log(this);
function z() {
"use strict";
console.log(this);
}
z();
}
y();
}
x();
4.2.2 第二道
let names = "小白";
function special() {
console.log('姓名', this.names);
}
let girl = {
names: '小红',
detail: function() {
console.log('姓名', this.names);
},
women: {
names: '小黄',
detail:function() {
console.log('姓名', this.names);
}
},
special:special,
}
girl.detail();
girl.women.detail();
girl.special();
4.2.3 第三道
var name = '小红';
function a() {
var name = '小白';
console.log(this.name);
}
function d(i) {
return i();
}
var b = {
name: '小黄',
detail:function() {
console.log(this.name);
},
bibi: function() {
return function() {
console.log(this.name);
}
}
}
var c = b.detail;
b.a = a;
var e = b.bibi();
a();
c();
b.a();
d(b.detail);
e();
4.2.4 答案
解析
- 第一题
- 首先直接在全局调用函数x(),那么
this
是默认指向,即指向全局,所以y()
打印window
对象; - 第二个之所以为
undefined
,是因为开启了严格模式use strict
, 严格模式下, 事件处理函数内的this
为undefined
- 首先直接在全局调用函数x(),那么
- 第二题
- 前面的小红、小黄应该没问题,都是调用对象内的函数,属于隐式绑定。
- 第三个之所以为小红,是因为是在
girl
对象的基础上调用的special()
方法,那么this
就会指向真正调用它的对象,即指向girl
, 所以打印小红。
- 第三题
- 先看第一个直接在全局调用函数
a()
,this
默认指向window
, 所以第一个打印小红 - 第二个全局调用函数
c()
, 虽然c
是 等于b.detail
函数 , 但是实际调用者是c
,而c
的this
指向全局,所以还是小红 (b.detail和b.detail()
的区别是前者是一个函数,后者是函数的执行结果) - 第三个
b.a()
,同上,实际调用者是b
,所以this
指向b
, 所以打印小黄 - 第四个直接全局调用函数
d()
,而d()
指向全局,所以同样打印小红 - 第五个和第二个一样,
b.bibi()
返回的是一个匿名函数, 也就是e = function(){...}
,但是实际调用者是e
, 而e
是指向全局的,所以还是小红。
- 先看第一个直接在全局调用函数
5. 手撕 JSON.stringify
JSON.stringify
将js
对象转化成JSON
对象
JSON.parse
将JSON
对象转化成JS
的值或对象最常见的比如复杂引用类型的深拷贝的应用
JSON.parse(JSON.stringify(obj))
, 原理就是先通过前者将其转化为JSON
对象,而后再转换成一个新的JS
对象, 这样就会为其分配新的堆内存。
JSON.parse
有两个参数,第一个是JSON
对象,第二个是可选参数是一个函数可以看看下面的例子
const json = '{"result": true, "count": 2}';
const obj = JSON.parse(json);
console.log(obj.result, obj.count); // true 2
// 带第二个参数的情况
let result = JSON.parse('{"p": 5}', function (k, v) {
if(k === '') return v;
return v*2;
});
console.log(result); // {p: 10}
JSON.stringify
有三个参数,第一个是待转换的对象,第二个是replacer
函数,第三个参数用来控制结果字符串里面的间距。
手撕
JSON.stringify
function jsonStringify(data) {
let type = typeof data;
if(type !== 'object') {
let result = data;
// data可能是基础数据类型的情况处理
if(Number.isNaN(data) || data === Infinity) {
result = "null"; // NaN 和 Infinity序列化返回null
}else if(type === 'function' || type === 'undefined' || type === 'symbol') {
// 由于function 和 symbol序列化后返回undefined ,所以一起处理
return undefined;
}else if (type === 'string') {
result = '"' + data + '"';
}
return String(result);
}else if(type === 'object') {
if(data === null) {
return "null";
}else if(data.toJSON && typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON());
}else if(data instanceof Array) {
let result = [];
data.forEach((item, index) => {
if(typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
result[index] = "null";
}else {
result[index] = jsonStringify(item);
}
});
result = "[" + result + "]";
return result.replace(/'/g, "");
}else {
// 普通对象
let result = [];
Object.keys(data).forEach((item,index) => {
if(typeof item !== 'symbol') {
// 如果key是symbol对象,忽略
if(data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') {
// 键值如果是undefined、function、symbol为属性值时候,忽略
result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
}
}
});
return ("{" + result + "}").replace(/'/g, "");
}
}
}
测试
console.log('----测试jsonStringify------');
let obj2 = {
age: 18,
arr: ['asd', 123],
sym: Symbol(2),
fn: function(){
console.log('aa');
},
info: {
son: 'aaa',
age: 2,
xx: null,
unf: undefined,
inf: Infinity
}
}
console.log(jsonStringify(obj2) === JSON.stringify(obj2));
console.log(jsonStringify(obj2));
console.log(JSON.stringify(obj2));