一、JS 原始类型
js的原始类型有6种:
- boolean
- null
- undefined
- number
- string
- symbol
原始类型是值,故而原始类型没有函数可以使用。
- 问:那么为什么'1'.toString()可以正常使用呢?
- 答:此时'1'已经变成了String类型(对象)
- 问2:Number类型相加,存在0.1 + 0.2 !== 0.3
- 答:解决方式:1) 将数字转成整数 2) 用第三方库 mathjs.org/
- null是不是对象?
- 答:null不是对象,虽然 typeof null 会输出 object,但是将null识别成对象是js的历史遗留问题。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,
000
开头代表是对象,然而null
表示为全零,所以将它错误的判断为 object
- 答:null不是对象,虽然 typeof null 会输出 object,但是将null识别成对象是js的历史遗留问题。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,
二、 类型转换
js中只存在三种类型转换:
- 转换为布尔值
- 转换为数字
- 转换为字符串
2.1 转Boolean
在条件判断时,除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都转为true
,包括所有对象。
2.2 对象转原始类型
对象在转换类型的时候,会调用内置的 [[ToPrimitive]]
函数,
算法逻辑为:
- 如果已经是原始类型了,那就不需要转换了
- 如果需要转字符串类型就调用
x.toString()
,转换为基础类型的话就返回转换的值。不是字符串类型的话就先调用valueOf
,结果不是基础类型的话再调用toString
- 调用
x.valueOf()
,如果转换为基础类型,就返回转换的值 - 如果都没有返回原始类型,就会报错
2.3 四则运算符
加法运算符不同于其他几个运算符,它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
'a' + + 'b' // -> "aNaN" +'b'等于NaN,故变成了字符串拼接
对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
比较运算符
- 如果是对象,就通过
toPrimitive
转换对象 - 如果是字符串,就通过
unicode
字符索引来比较
三、this指针
2.1 基本判断
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
2.2 箭头函数
箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的this
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
// 此时箭头函数的this指向了window
2.3 使用bind()改变上下文关系
bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。- 不管我们给函数
bind
几次,this
永远由第一次bind
决定
// 多个bind,this指向第一次绑定
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => Window
let a = { name: 'yck' }
function foo() {
console.log(this.name) // => 'yck'
console.log(this) // => a
}
foo.bind(a)()
2.4 优先级
多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this
最终指向哪里。
箭头函数的 this
一旦被绑定,就不会再被任何方式所改变。
new
的方式优先级最高- 接下来是
bind
这些函数 - 然后是
obj.foo()
这种调用方式, - 最后是
foo
这种调用方式
四、== 和 === 的区别
对于 == 来说,若对比双方的类型不一致,会进行类型转换。而 === 则是判断类型和值是否相同
五、变量提升
提升存在的根本原因就是为了解决函数间互相调用的情况。将变量和函数都提升到文件顶部
5.1 什么是变量提升
例子中发现,虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量
原因是变量被提升到了头部,这就叫做变量提升
console.log(a) // undefined
var a = 1
// 实际:
var a;
console.log(a); // undefined
a = 1;
5.2 暂时性死区
而使用 let 或者 const, 则会报错
console.log(a) // js会报错,意识使用了未定义的变量a
let a = 1
ES6规定,let/const
命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错。这也叫做暂时性死区
5.3 函数提升
提升存在的根本原因就是为了解决函数间互相调用的情况,若没有函数提升,则无法实现函数互相调用。
function test1() {
test2()
}
function test2() {
test1()
}
test1()
5.4 总结
- 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
var
存在提升,我们能在声明之前使用。let
、const
因为暂时性死区的原因,不能在声明前使用var
在全局作用域下声明变量会导致变量挂载在window
上,其他两者不会,但 let 和 const 依然会存在全局作用域中,可以被当作全局对象使用let
和const
作用基本一致,但是后者声明的变量不能再次赋值
六、模块化
6.1 好处
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
6.2 使用立即执行函数
(function(param){
globalVariable.test = function() {}
// ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
使用立即执行函数,可以做到:
- 不必为函数重命名,避免污染全局变量
- 形成独立的作用域,可以封装一些外部无法读取的私有变量
著名面试题:
<body>
<ul id="list">
<li>公司简介</li>
<li>联系我们</li>
<li>营销网络</li>
</ul>
<script>
var list = document.getElementById("list");
var li = list.children;
for(var i = 0 ;i<li.length;i++){
( function(param){
li[param].onclick = function(){
alert(param);
})(i); 把实参i赋值给形参param
}
}
</script>
</body>
6.3 CommonJS
CommonJS是nodejs也就是服务器端广泛使用的模块化机制。
该规范的主要内容是,模块必须通过module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。
特点:
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序
一般CommonJS用于nodejs的服务器端,前端要使用的话需要额外引入require.js
6.4 ES Module
ES6正式提出了内置的模块化语法,我们在浏览器端无需额外引入requirejs来进行模块化。
ES6中的模块有以下特点:
- 模块自动运行在严格模式下
- 在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域的内部存在;
- 模块顶级作用域的 this 值为 undefined
- 对于需要让模块外部代码访问的内容,模块必须导出它们