前端知识梳理之JS基础篇
JS数据类型
JS原始数据类型有哪些?引用数据类型有哪些?
在 JS 中,存在着 6 种原始值,分别是:
- boolean
- null
- undefined
- number
- string
- symbol
引用数据类型: 对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)
说出下面运行的结果,解释原因。
1 | function test(person) { |
结果:1
2p1:{name: “fyq”, age: 26}
p2:{name: “hzj”, age: 18}
原因: 在函数传参的时候传递的是对象在堆中的内存地址值,test函数中的实参person是p1对象的内存地址,相当于传引用. 所以通过调用person.age = 26 改变了p1的值,但随后person变成了另一块内存空间的地址,并且在最后将这另外一份内存空间的地址返回,赋给了p2。
null究竟是不是对象?
null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object。为了避免这种问题,我们可以通过 Object.prototype.toString.call(xx)进行判断类型。
‘1’.toString()为什么可以调用?
其实在这个语句运行的过程中做了这样几件事情:
1 | var s = new String('1'); |
第一步: 创建String类实例。
第二步: 调用实例方法。
第三步: 执行完方法立即销毁这个实例。
整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String。
0.1+0.2为什么不等于0.3?
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。
数据类型如何检测
typeof 方法判断类型
对于原始类型来说,除了 null 都可以调用typeof显示正确的类型。1
2
3
4
5typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
但对于引用数据类型,除了函数之外,都会显示”object”。
1 | typeof [] // 'object' |
因此采用typeof判断对象数据类型是不合适的,采用instanceof会更好,instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true
1 | const Person = function() {} |
能不能手动实现一下instanceof的功能
能, 从原型链查找 是否匹配 ,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13function myInstanceof(left, right) {
//基本数据类型直接返回false
if(typeof left !== 'object' || left === null) return false;
//getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
//查找到尽头,还没找到
if(proto == null) return false;
//找到相同的原型对象
if(proto == right.prototype) return true;
proto = Object.getPrototypeof(proto);
}
}
测试:1
2console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true
Object.is和===的区别
目前发现的 是 Object 相较于 === 严格一些, 修正了一些特殊情况下的失误. 目前发现有 +0和-0,NaN和NaN。
源码如下:1
2
3
4
5
6
7
8
9
10Object.is = function(x, y) {
// SameValue algorithm
if (x === y) { // Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
return x !== 0 || 1 / x === 1 / y;
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
};
JS数据类型之隐式转换
[] == ![] 发生了一些什么?
首先 == 运算符,会将左右两边都需要转换为数字然后进行比较。
[]转换为数字为0。
![] 首先是转换为布尔值,由于[]作为一个引用类型转换为布尔值为true, 因此![]为false,进而在转换成数字,变为0。
0 == 0 , 结果为true
JS中类型转换有哪几种?
JS中,类型转换只有三种:
- 转换成数字
- 转换成布尔值
- 转换成字符串
转换具体规则如下:
![[width] [height] [title text [alt text]]](/img/translation_table.jpg)
== 和 ===有什么区别?
===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如’1’===1的结果是false,因为一边是string,另一边是number。
==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:
- 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
- 判断的是否是null和undefined,是的话就返回true
- 判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
- 判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较
- 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
1 | console.log({a: 1} == true);//false |
对象转原始类型是根据什么流程运行的?
对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:
- 如果Symbol.toPrimitive()方法,优先调用再返回
- 调用valueOf(),如果转换为原始类型,则返回
- 调用toString(),如果转换为原始类型,则返回
- 如果都没有返回原始类型,会报错
1 | var obj = { |
如何让if(a == 1 && a == 2)条件成立?
1 | var a = { |
可以发现 可以用上题 中的特性来解题
闭包的理解
定义
闭包是指有权访问另外一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数.且由于闭包会携带包含它的函数的作用域,过度使用会导致占用更多的内存.因此需谨慎使用闭包.
闭包的本质
1 | var a = 1; |
在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。就这么简单一件事情!
闭包产生的本质就是,当前环境中存在指向父级作用域的引用。还是举上面的例子:
1 | function f1() { |
这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。
那是不是只有返回函数才算是产生了闭包呢?、
回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此我们还可以这么做:1
2
3
4
5
6
7
8
9var f3;
function f1() {
var a = 2
f3 = function() {
console.log(a);
}
}
f1();
f3();
让f1执行,给f3赋值后,等于说现在f3拥有了window、f1和f3本身这几个作用域的访问权限,还是自底向上查找,最近是在f1中找到了a,因此输出2。
在这里是外面的变量f3存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变。
闭包的运用
1 | for(var i = 1; i <= 5; i ++){ |
这是一道很经典的 Es5 运用闭包 保存变量,当然 Es6 中针对这一点出现了革命性的变化 ,let 标签,让JS有函数作用域变为了块级作用域,用let后作用域链不复存在。 代码的作用域以块级为单位。代码如下:1
2
3
4
5for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}
js原型链
原型对象和构造函数是什么?
在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。
当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个proto属性,指向构造函数的原型对象。
原型链 是什么?
JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。
- 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
- 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
js 继承
第一种: 借助call
1 | function Parent1(){ |
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。
第二种: 借助原型链
1 | function Parent2() { |
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:
1 | var s1 = new Child2(); |
可以看到控制台:s1 和 s2 都被改变了. 明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。
那么如何规避 这个问题呢?
第三种:将前两种组合
1 | function Parent3 () { |
可以在控制台看到 之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?
第四种: 组合继承的优化1
1 | function Parent4 () { |
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:1
2
3var s3 = new Child4();
var s4 = new Child4();
console.log(s3)
可以从控制台看到 子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。
第五种(最推荐使用): 组合继承的优化1
1 | function Parent5 () { |
这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。
继承的问题
假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Car{
constructor(id) {
this.id = id;
}
drive(){
console.log("wuwuwu!");
}
music(){
console.log("lalala!")
}
addOil(){
console.log("哦哟!")
}
}
class otherCar extends Car{}
现在可以实现车的功能,并且以此去扩展不同的车。
但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。
如果让新能源汽车的类继承Car的话,也是有问题的,俗称”大猩猩和香蕉”的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了。
继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。
当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。
那如何来解决继承的诸多问题呢?
用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。
顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。1
2
3
4
5
6
7
8
9
10
11
12function drive(){
console.log("wuwuwu!");
}
function music(){
console.log("lalala!")
}
function addOil(){
console.log("哦哟!")
}
let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);
代码干净,复用性也很好。这就是面向组合的设计方式。