原始(Primitive)类型
涉及面试题:原始类型有哪几种?null 是对象嘛?
在js中,存在着6中原始值,分别是:
- boolean
- null
- undefind
- number
- string
- symbol(如果不懂symbol的,可以来这里看看)
首先原始类型存储的值是没有函数可以调用的,例如undefind.toString()
此时,你一定有疑问,'1'.toString()
是可以使用的.其实这种情况下'1'
已经不是原始类型了,而是被强制转成了String
类型,也就是对象类型,所以可以调用toString
函数.
(此处插播一条短消息,JS中String与string的区别
String是构造函数,而"string"是变量的一种类型.)
typeof String // "function"
typeof string // "undefined"
typeof "string" // "string"
除了会在必要的情况下强转类型以外,原始类型还有一些坑。
最为明显的就是js的0.1 + 0.2 !== 0.3
的精度小数问题了.
另外对与null
来说,很多人会认为他是个对象类型,其实他是错误的,虽然typeof null
会输出Object
,但是这只是JS存在的历史悠久BUG.在JS的最初版本中使用的是32位系统,为了性能考虑,使用低频存储变量的类型信息,000
开头表示对象,然而null
表示为全零,所以他错误的判断为Object
.虽然现在内部类型判断代码已经改变了,但是这个bug却一直流传了下来.
对象Object)类型
涉及面试题:对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?
在JS中除了,除了原始类型,其他就是对象类型了.对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针),当你创建了一个对象类型的时候,内存会开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针).
const a = []
对于常量a
来说,假设内存地址为#001
,那么在地址(指针)#001
上存放了值[]
,常量a
存放了地址(指针)#001
,再看下面的代码:
const a = []
const b = a
b.push(1)
当我们将变量赋值给另一个变量时,其实赋予的是原本变量的地址(指针),也就是说变量b
存放的地址(指针)是#001
,所以我们进行数据修改时,修改的是地址(指针)#001
下面的值,因而地址的值改变了,所以两个变量的值都发生了变化.
console.log(a) ---[1]
console.log(b) --[1]
接下来我们看函数参数是对象的变化情况
function test(person) {
person.age = 26
person = {
name: 'yyy',
age: 30
}
return person
}
const p1 = {
name: 'yck',
age: 25
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
对于以上代码:
- 首先,函数传参是传递对象的副本.
- 到函数内部修改参数的属性这步,我相信大家都知道了
p1
的值被改变了 - 但是我们重新为
person
分配了一个对象时就出现了分歧,请看下图
所以最后person
拥有了一个新的地址(指针),也就和p1没什么关系了,导致了最终两个变量是不同的.归根结底,就是在函数内person的地址被改变了,导致值被改变了(this指向).
typeof vs instanceof
涉及面试题:typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?
typeof
对于原始类型来说,除了null
都可以正确显示类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof
对于对象来说,除了函数都会显示object
,所以说typeof
并不能准确判断变量到底是什么类型.
如果我们想判断一个对象的准确类型,这时候可以用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 来判断类型是不行的,当然我们还是有办法让 instanceof 判断原始类型的
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
你可能不知道Symbol.hasInstance
是什么,其实就是自定义一个instanceof行为的东西,以上这段代码,就等同于 typeof 'hello world' === 'string'
,所以结果自然就是true了
类型转换
涉及面试题:该知识点常在笔试题中见到,熟悉了转换规则就不惧怕此类题目了
在js转化类型中,基本为
- 转化为布尔值
- 转化为数字
- 转化为字符串
转化类型大致表格如下:
转Boolean
在条件判断时,除了undefined、null、0、-0、false、NaN
,其他所有值都会为true,包括所有对象.
对象转原始类型
对象在转原始类型时会调用内置的[[ToPrimitive]]
函数,对于该函数来说,算法逻辑来说一般如下:
- 如果已经是原始类型了,就不需要转化了
- 调用
x.indexOf()
方法,如果转换为基本类型,就返回转换的值. - 调用
x.toString()
方法,如果转化为基本类型,就返回转换的值.
当然你也可以重写Symbol.toPrimitive
,该方法在转换原始类型时优先级最高
lat a = {
valueOf(){
return 1
},
toString(){
return '1'
},
[Symbol.toPrimitive](){
return 2
}
}
1 + a = // =>3
四则运算符
加法运算符不同于其他运算符,具备以下特点:
- 运算中有一方是字符串就会把另一方也转换为字符串
- 如果一方不是字符串或是数字,会把他转化成数字或字符串
1 + ‘1’ // 11
true + true // 2
4 + [1,2,3] // 41,2,3
如果你对答案有疑问的?,请看下面解释:
- 第一行触发特点一,将左侧数字转化成字符串,故字符串进行拼接,得到答案
11
- 第二行触发特点二,将两侧布尔值转换为数字
true => 1
,故得到答案1
- 第三行触发特点二,稍加理解(字符串=> 数字, 数字=>字符串),将数组包含的数字,调用数组的
toString
方法转换为字符串,'4' + '1,2,3' // => 41,2,3
对于加法还需要注意 这类表达式 'a' + + 'b' // => 'aNaN'
首先我们先执行+ 'b'
,尝试把字符串'b'
转换成数字,结果失败了,转化不了真实的数字,返回NaN
,所以得到答案aNaN
那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
this
涉及面试题:如何正确判断 this?箭头函数的 this 是什么
普通函数
function foo() {
console.log(this.a)
}
var a = 1
foo()
const obj = {
a: 2,
foo: foo
}
obj.foo()
const c = new foo()
简单对上面例子进行分析,
- 对于直接调用foo()来说,无论在何处调用,this永远指向window.
- 对于obj.foo(),我们只需要记住谁调用了函数,谁就是this,(例1,foo() = window.foo())
- 对于new来说(js继承),this永远被绑定在了c上面,不会别任何地方改变.
箭头函数
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
对于箭头函数来说,是没有this指向的,this永远指向外层的this,所以包裹箭头函数的a()函数的this就是函数内部的this,此处指向window,另外对箭头函数使用bind这类函数是无效的
call apply bind
call apply
在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。
function fruits() {}
fruits.prototype = {
color: "red",
say: function() {
console.log("My color is " + this.color);
}
}
var apple = new fruits;
apple.say(); //My color is red
但是如果我们有一个对象banana= {color : “yellow”} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:
banana = {
color: "yellow"
}
apple.say.call(banana); //My color is yellow
apple.say.apply(banana); //My color is yellow
所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。
apply和call的区别:
对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:
var func = function(arg1, arg2) {
};
调用如下:
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
获取数组中的最大值和最小值
var numbers = [5, 458 , 120 , -215 ];
var maxInNumbers = Math.max.apply(Math, numbers), //458
maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458
number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。
bind
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
bar(); // undefined
var func = bar.bind(foo);
func(); // 3
这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。