js基础学习
类型转换
- 隐式类型转换
凡是通过逻辑运算符(&& 、|| 、!)、运算符(+ ,- , * , /)、关系运算符(>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到类型同都会进行隐式转换。
'==' 的隐式类型转换规则
-
如果类型相同,无需进行转换
-
如果其中一个操作符是null或undefined,则另一个必须是null或undefined才会返回true,否则都返回false
-
如果其中一个是Symbol类型,则返回false
-
如果两个操作值为string和number,则转换成number进行对比
-
如果一个操作值是boolean,则转换为number
-
如果一个操作值是object且另一个是string、number或Symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换)。
null == undefined // true 规则2 null == 0 // false 规则2 '' == null // false 规则2 '' == 0 // true 规则4 字符串转隐式转换成Number之后再对比 '123' == 123 // true 规则4 字符串转隐式转换成Number之后再对比 0 == false // true 5规则 布尔型隐式转换成Number之后再对比 1 == true // true 5规则 布尔型隐式转换成Number之后再对比 var a = { value: 0, valueOf: function() { this.value++; return this.value; } }; // 注意这里a又可以等于1、2、3 console.log(a == 1 && a == 2 && a ==3); //true 规则6 Object隐式转换 // 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
'+'的隐式类型转换
'+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。
特殊的规则如下所示:
- 如果一个是string,另一个是undefined、null或boolean,则调用toString()方法进行字符串拼接。如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接。
- 如果其中有一个是数字,另外一个是 undefined、null、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。
- 如果一个是字符串一个是数字,按照字符串规则进行拼接
1 + 2 // 3 常规情况
'1' + '2' // '12' 常规情况
// 下面看一下特殊情况
'1' + undefined // "1undefined" 规则1,undefined转换字符串
'1' + null // "1null" 规则1,null转换字符串
'1' + true // "1true" 规则1,true转换字符串
'1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串
1 + undefined // NaN 规则2,undefined转换数字相加NaN
1 + null // 1 规则2,null转换为0
1 + true // 2 规则2,true转换为1,二者相加为2
1 + 1n // 错误 不能把BigInt和Number类型直接混合相加
'1' + 3 // '13' 规则3,字符串拼接
Object 的转换规则
对象转换的规则,会先调用内置的[ToPrimitive]函数,逻辑如下:
- 如果部署了 Symbol.toPrimitive 方法,优先调用再返回;
- 调用对象自身的valueOf方法,如果该方法返回原始类型的值(数值、字符串和布尔值),则直接对该值使用Number方法,不再进行后续步骤。
- 如果valueOf方法返回复合类型的值,再调用对象自身的toString方法,如果toString方法返回原始类型的值,则对该值使用Number方法,不再进行后续步骤。
- 如果toString方法返回的是复合类型的值,则报错。
var obj = {
value: 1,
valueOf() {
return 2;
},
toString() {
return '3'
},
[Symbol.toPrimitive]() {
return 4
}
}
console.log(obj + 1); // 输出5
// 因为有Symbol.toPrimitive,就优先执行这个;如果Symbol.toPrimitive这段代码删掉,则执行valueOf打印结果为3;如果valueOf也去掉,则调用toString返回'31'(字符串拼接)
// 再看两个特殊的case:
10 + {}
// "10[object Object]",注意:{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,参考'+'的规则C
[1,2,undefined,4,5] + 10
// "1,2,,4,510",注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则,参考'+'的第3条规则
- 判断类型
function getType(obj) {
let type = typeof obj;
if (type !== 'object') {
return obj
}
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
}
深拷贝和浅拷贝
-
浅拷贝理解
创建一个新对象,来接收你要复制或者你用的对象值。如果对象属性是基本数据类型,复制的就是基本数据类型给对象,如果对象属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了,会影响另一个对象。
浅拷贝的方法
-
object.assign
Object.assign是ES6中Object的一个方法,可以用于js对象的合并等多个用途,也可以进行浅拷贝,该方法等第一个参数是拷贝的目标对象,后面的参数是拷贝的目标对象(也可以是多个来源)。
// 语法 object.assign(target,...sources)
有几个注意点
- 它不会拷贝对象的继承属性
- 它不会拷贝对象的不可枚举的属性
- 可以拷贝Symbol的属性
let obj1 = { a: { b: 1 }, sym:Symbol(1) }; Object.defineproperty(obj1, 'innumerable', { value: '不可枚举属性', enumerable: false }); let obj2 = {}; object.assign(obj2, obj1); obj1.a.b = 2; console.log('obj1',obj1); console.log('obj2',obj2);
从上面的样例代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能。
-
扩展运算符方式
// 语法 let cloneObj = {...obj}
扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
-
concat 拷贝数组
只能拷贝数组。
-
slice拷贝数组
仅针对数组,截取数组返回一个新的数组对象。两个参数分别代表原数组的开始和结束位置。
// 语法为: arr.slice(begin, end);
浅拷贝简单实现
const shallowClone = target => { if(typeof target === 'object' && typeof target !== 'null') { const cloneTarget = Array.isArray(target) ? [] : {} for(i in target) { if(target.hasOwnProperty(i)) { cloneTarget[i] = target[i] } } return cloneTarget } else { return target } }
-
-
深拷贝的原理和实现
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的属性无法被拷贝。深拷贝对于复杂类型的拷贝,是在堆内存中开辟了新的一块内存地址,并将原有的对象完全复制过来存放。
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
深拷贝的方法
-
JSON.stringify
JSON.stringify是目前开发中最简单的深拷贝方法,其中之一就是把对象序列化成为JSON字符串,并将对象里面的内容转化成字符串,最后再用JSON.parse()的方法将JSON 字符串生成一个新的对象。
但是使用 JSON.stringify 实现深拷贝还是有一些地方值得注意:
- 拷贝的对象中值如果有函数、undefined、symbol这几种类型,经过JSON.stringify之后的字符串中,键值对都会消失。
- 拷贝Date引用类型会变成字符串。
- 无法拷贝不可枚举的属性。
- 无法拷贝对象的原型链。
- 拷贝RegExp引用类型会变成空对象。
- 对象中含有NaN、infinity、-infinity,JSON序列化的结果会变成null。
- 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
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);
-
递归实现
该怎么做:
- 针对能够遍历对象的不可枚举属性以及Symbol类型,我们可以使用Reflect.ownKeys方法。
- 当参数是Date和RegExp类型,直接生成一个新的实例返回。
- 利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性,以及对应的特性,结合Object的create方法,创建一个新对象,并继承传入原对象的原型链。
- 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null) const deepClone = function (obj, hash = new WeakMap()) { if (obj instanceof 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)
-
对象
-
new操作符都做了些什么
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新的对象添加属性)
- 返回新对象
实现一个new
function myNew(fn) { // 创建一个新对象 let obj = new Object(); // 取出参数中的第一个参数,获得构造函数 let consturc = Array.prototype.shift.call(arguments) // 下列示例中的第一个参数Person // 连接原型,新对象可以访问原型中的属性 obj.__protp__ = contruc.prototype // 执行构造函数,即绑定 this,并且为这个新对象添加属性 let result = contruc.apply(obj,arguments) return result instanceof Object ? result : obj } function Person (name,age){ this.name = name; this.age = age; this.say = function () { console.log("I am " + this.name) } } let person1 = new Person("Star",20); console.log(person1.name); console.log(person1.age); person1.say(); let person2 = myNew (Person,"Star",20); console.log(person2.name); console.log(person2.age); person2.say();
对象继承实现
-
原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数(constructor)都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。
function Parent1 = function() { this.name = 'parent1'; this.play = [1,2,3]; } function Child1() { this.type = 'child2'; } Child1.prototype = new Parent1() console.log(new Child1())
缺陷:多个实例使用的是同一原型对象,他们的内存空间是共享的,当一个发生变化,全部都发生变化。