1、typeof类型判断
typeof 是否能正确判断类型? instanceof 能正确判断对象的原理是什么
- typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof 对于对象来说,除了函数都会显示 object ,所以说 typeof 并 不能准确判断变量到底是什么类型
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
如果我们想判断⼀个对象的正确类型,这时候可以考虑使⽤ instanceof , 因为内部机制是通过原型链来判断的
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
对于原始类型来说,你想直接通过 instanceof 来判断类型是不⾏的
2、类型转换
⾸先我们要知道,在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串 转Boolean
在条件判断时,除了 undefined , null , false , NaN , '' , 0 , -0 ,其他所有值都转为 true ,包括所有对象 对象转原始类型 对象在转换类型的时候,会调⽤内置的
ToPrimitive函数,对于该函数来说,算法逻辑⼀般来说如下
- 如果已经是原始类型了,那就不需要转换了
- 调⽤ x.valueOf() ,如果转换为基础类型,就返回转换的值
- 调⽤ x.toString() ,如果转换为基础类型,就返回转换的值
- 如果都没有返回原始类型,就会报错
当然你也可以重写 Symbol.toPrimitive ,该⽅法在转原始类型时调⽤优先级最⾼。
var a = {
valueOf() {
return 0;
},
toString() {
return '1';
},
[Symbol.toPrimitive]() {
return 2;
},
};
console.log(1 + a); // 3
var b = {
valueOf() {
return 11;
},
toString() {
return 10;
},
};
console.log(1 + b); // 12
var c = {
toString() {
return 5;
},
};
console.log(1 + c); // 6
四则运算符
它有以下⼏个特点:
- 运算中其中⼀⽅为字符串,那么就会把另⼀⽅也转换为字符串
- 如果⼀⽅不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
- 对于第⼀⾏代码来说,触发特点⼀,所以将数字 1 转换为字符串,得到结果 '11'
- 对于第⼆⾏代码来说,触发特点⼆,所以将 true 转为数字 1
- 对于第三⾏代码来说,触发特点⼆,所以将数组通过 toString 转为字符串 1,2,3 ,得到结果 41,2,3
另外对于加法还需要注意这个表达式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
- 因为 + 'b' 等于 NaN ,所以结果为 "aNaN" ,你可能也会在⼀些代码中看到过 +'1' 的形式来快速获取 number 类型。
- 那么对于除了加法的运算符来说,只要其中⼀⽅是数字,那么另⼀⽅就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
⽐较运算符
- 如果是对象,就通过 toPrimitive 转换对象
- 如果是字符串,就通过 unicode 字符索引来⽐较
let a = {
valueOf() {
return 0;
},
toString() {
return '1';
},
};
a > -1; // true
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再⽐较值。
3、This
我们先来看⼏个函数调⽤的场景
function foo() {
console.log(this.a);
}
var a = 1;
foo();
const obj = {
a: 2,
foo: foo,
};
obj.foo();
const c = new foo();
- 对于直接调⽤ foo 来说,不管 foo 函数被放在了什么地⽅, this ⼀定是 window
- 对于 obj.foo() 来说,我们只需要记住,谁调⽤了函数,谁就是 this ,所以在这个场景下 foo 函数中的 this 就是 obj 对象
- 对于 new 的⽅式来说, this 被永远绑定在了 c 上⾯,不会被任何⽅式改变 this
说完了以上⼏种情况,其实很多代码中的 this 应该就没什么问题了,下⾯ 让我们看看箭头函数中的 this
function a() {
return () => {
return () => {
console.log(this);
};
};
}
console.log(a()()());
- ⾸先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第⼀个普通函数的 this 。在这个例⼦中,因为包裹箭头函数的第⼀个普通函数是 a ,所以此时的 this 是 window 。另外对箭头函数使⽤ bind 这类函数是⽆效的。
- 最后种情况也就是 bind 这些改变上下⽂的 API 了,对于这些函数来说, this 取决于第⼀个参数,如果第⼀个参数为空,那么就是 window 。
- 那么说到 bind ,不知道⼤家是否考虑过,如果对⼀个函数进⾏多次 bind ,那么上下⽂会是什么呢?
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?
如果你认为输出结果是 a ,那么你就错了,其实我们可以把上述代码转换成另⼀种形式
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function () {
return fn.apply();
}.apply(a);
};
fn2();
可以从上述代码中发现,不管我们给函数 bind ⼏次, fn 中的 this 永远由第⼀次 bind 决定,所以结果永远是 window
let a = { name: 'poetries' };
function foo() {
console.log(this.name);
}
foo.bind(a)(); // => 'poetries'
以上就是 this 的规则了,但是可能会发⽣多个规则同时出现的情况,这时候不同的规则之间会根据优先级最⾼的来决定 this 最终指向哪⾥。
⾸先, new 的⽅式优先级最⾼,接下来是 bind 这些函数,然后是obj.foo() 这种调⽤⽅式,最后是 foo 这种调⽤⽅式,同时,箭头函数的this ⼀旦被绑定,就不会再被任何⽅式所改变。
4、== 和 === 有什么区别
对于 == 来说,如果对⽐双⽅的类型不⼀样的话,就会进⾏类型转换 假如我们需要对⽐ x 和 y 是否相同,就会进⾏如下判断流程
- ⾸先会判断两者类型是否相同。相同的话就是⽐⼤⼩了
- 类型不相同的话,那么就会进⾏类型转换
- 会先判断是否在对⽐ null 和 undefined ,是的话就会返回 true
- 判断两者类型是否为 string 和 number ,是的话就会将字符串转换为 number
1 == '1' -> 1 == 1
- 判断其中⼀⽅是否为 boolean ,是的话就会把 boolean 转为 number 再进⾏判断
'1' == true -> '1' == 1 -> 1 == 1
- 判断其中⼀⽅是否为 object 且另⼀⽅为 string 、 number 或者 symbol ,是的话就会把 object 转为原始类型再进⾏判断
'1' == { name: 'yck' } -> '1' == '[object Object]'
对于 === 来说就简单多了,就是判断两者类型和值是否相同
5、闭包
闭包的定义其实很简单:函数 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);
}
⾸先因为 setTimeout 是个异步函数,所以会先把循环全部执⾏完毕,这时候 i 就是 6 了,所以会输出⼀堆 6 解决办法有三种
- 第⼀种是使⽤闭包的⽅式
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
在上述代码中,我们⾸先使⽤了⽴即执⾏函数将 i 传⼊函数内部,这个时候值就被固定在了参数 j 上⾯不会改变,当下次执⾏ timer 这个闭包的时候,就可以使⽤外部函数的变量 j ,从⽽达到⽬的
- 第⼆种就是使⽤ setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传⼊
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j);
},
i * 1000,
i
);
}
- 第三种就是使⽤ let 定义 i 了来解决问题了,这个也是最为推荐的⽅式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
6、深浅拷⻉
浅拷⻉
⾸先可以通过 Object.assign 来解决这个问题,很多⼈认为这个函数是⽤来深拷⻉的。其实并不是, Object.assign 只会拷⻉所有的属性值到新的对象中,如果属性值是对象的话,拷⻉的是地址,所以并不是深拷⻉
let a = {
age: 1,
};
let b = Object.assign({}, a);
a.age = 2;
console.log(b.age); // 1
另外我们还可以通过展开运算符 ... 来实现浅拷⻉
let a = {
age: 1,
};
let b = { ...a };
a.age = 2;
console.log(b.age); // 1
通常浅拷⻉就能解决⼤部分问题了,但是当我们遇到如下情况就可能需要使⽤到深拷⻉了
let a = {
age: 1,
jobs: {
first: 'FE',
},
};
let b = { ...a };
a.jobs.first = 'native';
console.log(b.jobs.first); // native
浅拷⻉只解决了第⼀层的问题,如果接下去的值中还有对象的话,那么就⼜回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使⽤深拷⻉了。
深拷⻉
这个问题通常可以通过 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
但是该⽅法也是有局限性的:
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引⽤的对象
7、原型
原型链就是多个对象通过
__proto__的⽅式连接了起来。为什么obj可以访问到valueOf函数,就是因为obj通过原型链找到了valueOf函数
Object是所有对象的爸爸,所有对象都可以通过__proto__找到它Function是所有函数的爸爸,所有函数都可以通过__proto__找到它- 函数的
prototype是⼀个对象 - 对象的
__proto__属性指向原型,__proto__将对象和原型连接起来组成了原型链
8、var、let 及 const 区别
涉及⾯试题:什么是提升?什么是暂时性死区?var、let 及 const 区别?
- 函数提升优先于变量提升,函数提升会把整个函数挪到作⽤域顶部,变量提升只会把声明挪到作⽤域顶部
var存在提升,我们能在声明之前使⽤。let const因为暂时性死区的原因,不能在声明前使⽤var在全局作⽤域下声明变量会导致变量挂载在 window 上,其他两者不会let和const作⽤基本⼀致,但是后者声明的变量不能再次赋值