多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
### 原始类型 1. js原始类型有哪几种?null是对象吗? * 6中原始类型(undefined,null,boolean,string,number,symbol) * 原始类型存储的都是值,没有函数可以调用。 ~~~ **undefined.toString()//报错** "1".toString()是可以使用的,”1“不是原始类型了,被强制转换成String类型(对象类型) console.log(0.1 + 0.2 !== 0.3) //true ~~~ * null不是对象类型, 虽然 `typeof null =》object` symbol:ES6引入,表示独一无二的值,通过Symbol函数生成,Symbol函数前不能使用new命令,否则会报错,因为生成的Symbol是一个原始类型,不是对象(不能添加属性),是一种类似字符串的数据类型。 Symbol值可以作为标识符,用于对象的属性名,保证不会出现同名的属性。 ~~~ const shapeType = { triangle: Symbol() }; Object.getOwnPropertySymbols ~~~ ### 对象类型 1. 对象类型和原始类型的不同?函数参数是对象发生的问题? * 不同:原始类型存储的是值,对象类型存储的是地址(指针),创建一个对象类型时,计算机会在内存中开辟一个空间来存放值,这个空间拥有一个地址(指针) 题型: ~~~ function test(person){ person.age = 26; person = { name:'yyy', age:30 } return person } const p1 ={ name:'xxx', age: 1} const p2 = test( p1 ); console.log(p1);//{name:'xxx',age:26} console.log(p2) //{name:'yyy',age:30} ~~~ * 函数传参是传递**对象指针**的副本(地址) ### typeof vs instanceof vs toString 1. typeof是否能正确判断类型?instanceof能正确判断对象的原理是什么? * typeof对原始类型来说,除了null都可以显示正确的类型 ~~~ typeof 1 //number typeof "1" //string typeof undefined //undefined typeof true //boolean typeof Symbol() //symbol ~~~ * typeof对于对象,除了函数(function)都会显示object * typeof不能准确判断变量到底是什么类型 * 判断对象的正确类型,使用instanceof(原始类型不鞥呢通过instanceof直接判断) ~~~ const Person = function(){} const p1 = new Person(); p1 instancof Person //true var str = 'hello world'; str instanceof String //false var str = new String('xxx'); str1.instanceof String //true ~~~ * 补充 instanceof判断原始类型(instanceof 也不是百分百可信的) ~~~ class PrimitiveString { static [Symbol.hasInstance](x) { return typeof x === 'string' } } console.log('hello world' instanceof PrimitiveString) // true ~~~ * 判断利器: **Object.prototype.toString.call**(对象的原生扩展函数,精确区分数据类型) ~~~ var getType = Object.prototype.toString getType.call('aaa') //[object String] getType.call(222) //[object Number] getType.call(true) //[object Boolean] getType.call(undefined) //[object Undefined] getType.call(null) //[object Null] getType.call({}) //[object Object] getType.call([]) //[object Array] getType.call(function(){}) //[object Function] ~~~ * ### 类型转换 * 三种情况(转为布尔,转为数字,转为字符串) * 常见转换, ~~~ 0,-0,NaN,undefined,null =>Boolean(false) Boolean,函数,Symbol=》String('true') 数组([1,2])=>String( '1,2' ) 对象 =》String([object Object]) 字符串=》数字('1'=>1 'a'=>NaN) 数组=》数字(空数组为0,存在一个元素且为数字转数字,其他情况NaN) null = 》数字(0)( 但是null != 0) 除了数组的引用类型 =》 数字(NaN) Symbol = 》数字(抛错) ~~~ * 转Boolean(undefined,null,false,NaN,'',0,-0=》 false) * 对象转原始类型 #### 四则运算 1. 加法运算规则 * 有一方为字符串,会把另一方转为字符串 * 如果一方不是字符串或数字,那么会将它转为数字或字符串 ~~~ true +true //2 //都不是字符串,转为数字 true + "x" //truex 4+[1,2,3] //41,2,3 //都不是字符串,后者只能转为字符串 'a' + +'b' //'aNaN' (+'b'=>NaN) ~~~ * 除了加法运算,只有其中一方是数字,那么另一方就会被转为数字 ~~~ 4 * '3' //12 4*[] //0 4*[1,2] //NaN ~~~ #### 比较运算 * 如果是对象,就通过 toPrimitive转对象 toPrimitive:将对象转成原始值,如果是原始值,直接返回;否则调用valueOf()(求值),如果结果是原始值,返回;否则调用toString()(求字符串),如果是原始值,返回;否则抛错 * 如果是字符串,通过unicode字符索引来比较 * valueOf() toString() ### this * 如何正确判断this?箭头函数的this是什么? ~~~ function foo(){ console.log(this.a); } var a =1; foo(); // this指 window 1 const obj = { a: 2, foo: foo } obj.foo(); // this 指obj 2 const c = new foo(); //c undefined c.foo() ~~~ 分析: * 直接调用foo,this指向window * obj.foo():谁调用了函数,谁就是this * new方式,this被永远绑定在c上 箭头函数的this ~~~ function a(){ console.log(this); return ()=>{ console.log(this); return ()=>{ console.log(this) } } } console.log(a()()) //window window window ~~~ * 首先,箭头函数其实是没有this的,箭头函数中的this值取决于包裹箭头函数的第一个普通函数的this。包裹箭头函数的第一个普通函数是a,所以this是window。另外,**对箭头函数使用bind这类函数是无效的**。 **改变上下文的API:** bind,call,apply * 对于这些函数,this取决于第一个参数,如果第一个参数为空,this指向window * 多次bind,不管我们给函数bind几次,fn中的this永远有第一次bind决定 ~~~ let a = {}; let fn = function(){ console.log(this)} fn.bind().bind(a); //window ~~~ 等于 ~~~ // fn.bind().bind(a) 等于 let fn2 = function fn1() { return function() { return fn.apply() }.apply(a) } fn2() ~~~ * 多个规则同时出现,根据优先级来决定this指向哪里 n**ew的方式优先级最高,接下来是bind,然后是obj.foo(),最后是foo这种调用方式,同时,箭头函数this一旦被绑定,就不会被任何方式改变**。 ![](https://box.kancloud.cn/9061aa32ca1fd1e4b3e8476c5e595aae_651x476.png) ### == vs ===(有什么区别?) ==:如果双方的类型不一样的话,就会进行**类型转换** === :判断类型和值是否都相同 判断流程: 1. 判断两者类型是否相同。相同比大小 2. 类型不同,进行类型转换 3. 先判断是否对比 **null和undefined**,是的话就会返回true 4. 判断两者类型是否为**string和number,是的话将字符串转为number** 5. 判断其中一方为**boolean,是的话把boolean转为number**(true=>1) 6. 判断其中一方是否为object,且另一方为string、number或者symbol,是的话就把object转为原始类型再进行判断 ~~~ '1' == {name:'xxx'} 转为: '1' == '[object Object]' ~~~ 7. `[] == ![] ` 分析: ~~~ [] == ![] 转为: [] == false 一方为boolean类型,把boolean类型转为 number =》 [] == 0;//一个操作数是对象,另一个不是,则调用对象的(valueOf toString)方法 转为 '' == 0 因此为true!!! ~~~ ~~~ {} == !{} !{} =》 false =》{} == false =》 {} == 0 => 'object' {} == {} //同为对象类型,肯定不等 ~~~ ![](https://box.kancloud.cn/a7f1f2ac796ad63a0e855096f48d3b91_692x297.png) ### 闭包 定义:函数A内部有一个函数B,函数B可以访问到函数A中的变量,那么函数B就是闭包 不完整定义:函数嵌套函数,然后返回一个函数,下面这个例子可以反驳 ~~~ function A(){ let a = 1; window.B = function(){ console.log(a); } } A(); B();// 1 ~~~ 经典面试题,解决var定义函数的问题 ~~~ for(var i = 1; i<=5; i++){ setTimeout(function timer(){ console.log(i) }, i*1000) } // 5 5 5 5 5 ~~~ setTimeout是异步函数,先把循环全部执行完毕,这是` i是6`,输出一堆6 解决办法有三种 1. 闭包的形式 ~~~ for(var i = 1; i<=5; i++){ (function(j){ setTimeout(function timer(){ console.log(j) }, j*1000) })(i) } // 1 2 3 4 5 ~~~ 首先使用立即执行函数,将i传入函数内部,这个时候就被固定在参数j上面不会改变 2. 使用setTimeout的第三个参数,这个参数会被当成timer函数的参数传入 ~~~ for(var i = 1; i<=5; i++){ setTimeout(function timer( j ){ console.log(j) }, i*1000 ,i ) } ~~~ 3. 使用let定义i,推荐 ~~~ for(let i = 1; i<=5; i++){ setTimeout(function timer(){ console.log(i) }, i*1000) } ~~~ ### 深浅拷贝 **题目:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?** 对象类型在赋值过程中,其实是复制了地址,改变一方其他都被改变。 1. 浅拷贝 * `Object.assign`:只会拷贝所有的属性值到新的对象,如果属性值是对象的话,拷贝的是地址,所有并不是深拷贝 ~~~ let a = { age : 1}; let b = Object.assign({},a); a.age = 2; console.log(b.age) //1 ~~~ 特例: ~~~ a = { age : 1,test:{test:111}}; b = Object.assign({},a); a.test.test = 222; console.log(b.test.test) //1 ~~~ * `...`运算符实现浅拷贝 ~~~ let a = { age :1 } let b = { ..a }; a.age = 2; console.log(b.age)//1 ~~~ 特例: ~~~ a = { age :1 ,test:{test:111} } b = { ...a }; a.test.test = 222; console.log(b.test.test)//222 ~~~ 通常浅拷贝就能解决大部分问题,但是有时可能需要使用深拷贝 ~~~ let a = { age: 1, jobs: { first: 'FE' } } let b = { ...a } a.jobs.first = 'native' console.log(b.jobs.first) // native ~~~ * 浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,两者享有相同地址。要解决这个问题,就得使用深拷贝。 2. 深拷贝 * `JSON.parse(JSON.stringify(object))` 通常通过这个来解决 ~~~ let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE ~~~ 问题局限性: 1. 会忽略undefined 2. 会忽略symbol 3. 不会序列化函数 4. 不能解决循环引用的对象 5. 没有null ~~~ let obj = { a: 1, b:{ c:2, d:3}} obj.c = obj.b; obj.e = obj.a; obj.b.c = obj.b; obj.b.e = obj.b.c; let newObj = JSON.parse(JSON.stringify(obj)) console.log(newObj) ~~~ 会报错 ![](https://box.kancloud.cn/409972a6fd757e514a8a08bc4a4d0b25_578x98.png) * 在遇到函数、`undefined`或者`symbol`的时候,该对象也不能正常的序列化(没有null) ~~~ let a = { age: undefined, sex: Symbol('male'), jobs: function() {}, name: 'yck' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "yck"} ~~~ 复杂数据都是可以序列化的,这个函数可以解决大部分问题 如果你所需要拷贝的对象含有内置类型并且不包含函数,可以使用MessageChanel ~~~ function structuralClone(obj) { return new Promise(resolve => { const { port1, port2 } = new MessageChannel() port2.onmessage = ev => resolve(ev.data) port1.postMessage(obj) }) } var obj = { a: 1, b: { c: 2 } } obj.b.d = obj.b // 注意该方法是异步的 // 可以处理 undefined 和循环引用对象 const test = async () => { const clone = await structuralClone(obj) console.log(clone) } test() ~~~ 实现一个深拷贝,但实现一个深拷贝是很困难的,需要考虑好多种边界情况,比如原型链如何处理,DOM如何处理等,我们实现的深拷贝只是简易版,并且推荐使用**lodash的深拷贝函数** ~~~ function deepClone(obj){ function isObject(o){ return (typeof 0 ==='object' || typeof o ==='function') && o != null) } if(!isObjet(object)){ throw new Error('对象') } let isArray = Array.isArray(obj); let newObj = isArray ? [...obj] : { ...obj}; Reflect.ownKeys(newObj).forEach( key =>{ newObj[key] = isObject(obj[key])?deepClone(obj[key]) :obj[key] }) return newObj; } let obj = { a:[1,2,3], b:{c:2,d:3}, } let new Obj = deepClone(obj); newObj.b.c = 1; console.log(obj.b.c) //2 ~~~ `Object.keys()`返回属性key,但不包括不可枚举的属性 `Reflect.ownKeys()`返回所有属性key ### 原型 **如何理解原型?如何理解原型链?** 每个js对象都有`__proto__`属性,这个属性指向了原型。不推荐直接使用 ![](https://box.kancloud.cn/e57fbd35931c93b7e6e9e142d7217b06_408x283.png) 可以发现一个`constructor属性`,就是构造函数 ![](https://box.kancloud.cn/7642682bd6a668057f453fdb9c63cabb_578x411.png) 打开constructor属性,可以发现一个`prototype`属性,这个属性和`__proto_`_对应。 结论:**原型的constructor属性指向构造函数,构造函数又通过prototype属性指向原型,但是并不是所有函数都具有这个属性,Function.prototype.bind()就没有这个属性。** ![](https://box.kancloud.cn/3962ef83481b4cd5b4ce71058ab4a67b_478x599.png) **原型链就是多个对象通过`__proto__`的方式连接起来的。**(我们创建一个对象,发现能使用很多没有定义过的函数)(为什么obj可以访问到valueOf函数,就是因为obj通过原型链找到了valueOf函数) 总结: 1. ` Object`是所有对象的爸爸,所有对象可以通过`__proto__`找到它 2. Function是所有函数的爸爸,所有函数可以通过`__proto__`找到它 3. 函数的prototype是一个对象 4. 对象的`__proto__`指向原型,`__proto__`将对象和原型两居起来组成了原型链