JS基础知识
主要知识点:
- 变量的类型与计算
- 原型与原型链
- 作用域与闭包
- 异步与单线程
1. 变量的类型与计算
题目
typeof
能判断哪些类型何时使用
==
何时使用===
值类型和引用类型的区别
js// 值类型和引用类型的区别 const obj1 = { x: 100, y: 200 } const obj2 = obj1 let x1 = obj1.x obj2.x = 101 x1 = 102 console.log(obj1) // 这里打印出什么?
手写深拷贝
知识点
变量类型
回顾JS的变量类型有哪些?
值类型与引用类型
// 值类型
let a = 100
let b = a
a = 200
console.log(b) // 100
// 引用类型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age) // 21
值类型与引用类型有什么区别?
值类型直接定在内存栈中, 引用类型将值定义在内存堆中并通过引用地址表示
哪些是值类型? 哪些是引用类型?
常见值类型
jsconst a // undefined const s = 'abc' const n = 100 const b = true const s = Symbol('s')
常见引用类型
jsconst obj = { x: 100 } const arr = ['a', 'b', 'c'] const n = null // 特殊引用类型,指针指向为空地址 function fn() {} // 特殊引用类型,但不用于存储数据,所以没有“拷贝、复制函数”这一说
如何判断类型?
typeof
-> 能判断所有值类型、能判断函数、能识别引用类型
// 判断所有值类型
let a
console.log(a) // 'undefined'
const str = 'abc'
typeof str // 'string'
const n = 100
typeof n // 'number'
const b = true
typeof b // 'boolean'
const s = Symbol('s')
typeof s // 'symbol'
// 能判断函数
typeof console.log // 'function'
typeof function () {} // 'function'
// 能识别引用类型(不能再继续识别)
typeof null // 'object'
typeof ['a', 'b'] // 'object'
typeof { x: 100 } // 'object'
变量计算
变量计算一般用于值类型,引用类型会通过 API 来修改数据。
- 数字 加减乘除
- 字符串 拼接
- 逻辑运算 && || ! == if-else
这其中,会隐含比较大的坑 —— 强制类型转换
字符串拼接( + 号)
const a = 100 + 10 // 110
const b = 100 + '10' // '10010'
const c = true + '10' // 'true10'
== 和 ===
// == 会尝试强制类型转换
100 == '100' // true
0 == '' // true
0 == false // true
false == '' // true
null == undefined // true
总之,==
会尝试进行强制类型转换,至于转换的规则大家没必要,只需要记住一点
- 所有的地方都用
===
- 除了判断是 null 或者 undefined 时用
if (obj.a == null)
—— 这也是 jQuery 源码中的方式
const obj = { x: 100 }
if (obj.a == null) { }
// 相当于:
if (obj.a === null || obj.a === undefined) { }
深拷贝
面试中经常会遇到浅拷贝与深拷贝有什么区别? 重点就是在于一个引用类型的考察以及对类型判断递归的使用
// 深拷贝
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null ) {
// 不是对象或者数组形式,或是 null ,直接返回
return obj
}
// 初始化返回结果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
// 变量
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 保证不是原型属性(原型和原型链部分会讲解)
// 递归调用
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
const obj1 = { x: 100, y: 200 }
const obj2 = deepClone(obj1)
obj1.x = 101
console.log(obj2) // x: 100
题目解答
JS中使用typeof
能得到的哪些类型
针对这个题目,可以通过以下程序进行验证
typeof undefined // 'undefined'
typeof 'abc' // 'string'
typeof 123 // 'number'
typeof true // 'boolean'
typeof Symbol('s') // 'symbol'
typeof {} // 'object'
typeof [] // 'object'
typeof null // 'object'
typeof console.log // 'function'
何时使用===
何时使用==
- 所有的地方都用
===
- 除了判断是 null 或者 undefined 时用
if (obj.a == null)
—— 这也是 jQuery 源码中的方式
const obj = { x: 100 }
if (obj.a == null) { }
// 相当于:
if (obj.a === null || obj.a === undefined) { }
值类型和引用类型的区别
// 值类型和引用类型的区别
const obj1 = { x: 100, y: 200 }
const obj2 = obj1
let x1 = obj1.x
obj2.x = 101
x1 = 102
console.log(obj1) // { x: 101, y: 200 }
2. 原型和原型链
JS是基于原型 prototype 继承的语言,故而在以往面试题中原型也是被常问的知识点。
在ES6 可使用类 class 继承(语法糖,本质还是原型继承)
**【重点提示】ES6 已经普及使用,原先构造函数的实现方式已经成为历史。**以讲解 class 为主,不会再从构造函数讲起。
题目
- 如何准确判断一个变量是数组类型
- 实现一个简易的 jQuery ,考虑插件和扩展性 —— PS: 虽然 jQuery 已应用不多,但借助学习原型非常好
- class 是语法糖,其本质是什么?
知识点
class
定义与使用
// 声明一个类
class Student {
constructor(name, number) {
// 属性
this.name = name
this.number = number
}
// 方法
sayHi() {
console.log(`姓名 ${this.name},学号 ${this.number}`)
}
}
// 用类来声明对象
let xialuo = new Student('夏洛', 100)
xialuo.sayHi()
let madongmei = new Student('马冬梅', 101)
madongmei.sayHi()
继承
// 父类
class People {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat something`)
}
}
// 子类
class Student extends People {
constructor(name, number) {
super(name)
this.number = number
}
sayHi() {
console.log(`姓名 ${this.name},学号 ${this.number}`)
}
}
// 子类
class Teacher extends People {
constructor(name, major) {
super(name)
this.major = major
}
teach() {
console.log(`${this.name} teach you ${this.major}`)
}
}
const xialuo = new Student('夏洛', 100)
xialuo.sayHi()
const wanglaoshi = new Teacher('王老师', '语文')
wanglaoshi.teach()
类型判断
引用类型使用 instanceof
xialuo instanceof Student // true
xialuo instanceof People // true
xialuo instanceof Object // true
[] instanceof Array // true
[] instanceof Object // true
{} instanceof Object // true
原理后面再看!!!
原型
通过typeof判断通过class定义的类
// class 实际上是函数,可见是语法糖
typeof People // 'function'
typeof Student // 'function'
// 隐式原型和显示原型
console.log( xialuo.__proto__ )
console.log( Student.prototype )
console.log( xialuo.__proto__ === Student.prototype )
显示原型和隐式原型的关系(可画图说明!!!)
- 每个 class 都有 prototype 显示原型
- 每个实例都有 proto 隐式原型
- 实例的 proto 指向对应 class 的 prototype
基于原型的执行逻辑
- 执行实例方法时,如
xiaoluo.sayHi()
- 会先从实例自身属性查找(可通过
hasOwnProperty
判断) - 如果找不到则自动去 proto 查找
原型链
以第二个例子为继承的例子。
console.log( Student.prototype.__proto__ )
console.log( People.prototype )
console.log( People.prototype === Student.prototype.__proto__ )
继续补充原型图示!!!
根据之前的规则,再去演练 xialuo.eat()
原型和原型链综合演练
根据以上规则和图示,综合演练
xialuo.name
xialuo.sayHi()
xialuo.eat()
继续延伸,xialuo.hasOwnProperty
从哪里得来? —— 继续补充原型图示!!!
题目解答
如何准确判断一个变量是数组类型
instanceof
实现一个简易的 jQuery ,考虑插件和扩展性
基本框架
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this[i] = selectorResult[i]
}
this.length = length
}
get(index) {
return this[index]
},
each(fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i]
fn(elem)
}
return this
},
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
}
插件机制
// 使用继承 —— 基于 jQuery 基本功能,再造一个更强大的轮子
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
},
style(data) {
}
}
// 使用原型 —— 还用 jQuery ,仅仅扩展一个功能而已
jQuery.prototype.dialog = function (info) {
console.log(this) // this 即 jQuery 对象
}
class 是语法糖,其本质是什么?
- 通过typeof判断得知为function
- 本质是构造函数、继承等操作都是基于原型去实现的
3. 作用域与闭包
作用域、闭包是一个头疼的玩意,特别对应初次接触者来说真可谓是头疼,作为面试他也是面试题中的常青树。
题目
this 的不同应用场景
创建 10 个
<a>
标签,点击的时候弹出来对应的序号js// 创建 10 个`<a>`标签,点击的时候弹出来对应的序号 let i, a for (i = 0; i < 10; i++) { a = document.createElement('a') a.innerHTML = i + '<br>' a.addEventListener('click', function (e) { e.preventDefault() alert(i) }) document.body.appendChild(a) }
实际开发中闭包的应用
手写 bind 函数
知识点
作用域
所谓作用域,即一个变量的合法使用范围
let a = 0
function fn1() {
let a1 = 100
function fn2() {
let a2 = 200
function fn3() {
let a3 = 300
return a + a1 + a2 + a3
}
fn3()
}
fn2()
}
fn1()
作用域分类
- 全局作用域:在全局定义的变量,全局都可用,像 document
- 函数作用域:在某个函数中定义的变量,只能用于当前函数,像 a b
- 块级作用域(ES6):只能活跃于当前的块,示例如下
// ES6 块级作用域
if (true) {
let x = 100
}
console.log(x) // 会报错
自由变量
- 一个变量在该作用域没有被定义,但被使用
- 向上级作用域去寻找该变量,层层往上找 —— 如上面的示例
let a = 0
function fn1() {
let a1 = 100
function fn2() {
let a2 = 200
function fn3() {
let a3 = 300
return a + a1 + a2 + a3
}
fn3()
}
fn2()
}
fn1()
闭包
闭包 —— 作用域应用的一个特殊情况。一般有两种书写形式:
- 函数作为参数被传入
- 函数作为返回值
// 函数作为返回值
function create() {
let a = 100
return function () {
console.log(a)
}
}
let fn = create()
let a = 200
fn()
// 函数作为参数
function print(fn) {
let a = 200
fn()
}
let a = 100
function fn() {
console.log(a)
}
print(fn)
从上面代码中可得出 函数中自由变量在使用时 **函数定义的地方(不是执行的地方)**去寻找上级作用域
这种现象称为闭包
闭包示例
隐藏定义的数据 只提供API
function createCache() {
let data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
let c = createCache()
c.set('a', 100)
console.log( c.get('a') )
this
常见场景:
- 作为普通函数调用
- 使用
call
apply
bind
- 作为对象方法调用
- 在 class 的方法中调用
- 箭头函数
// this 场景题 - 1
function fn1() {
console.log(this)
}
fn1() // 打印什么
fn1.call({ x: 100 }) // 打印什么
const fn2 = fn1.bind({ x: 200 })
fn2() // 打印什么
// this 场景题 - 2
const zhangsan = {
name: '张三',
sayHi() {
console.log(this)
},
wait() {
setTimeout(function() {
console.log(this)
})
},
waitAgain() {
setTimeout(() => {
console.log(this)
})
}
}
zhangsan.sayHi() // 打印什么
zhangsan.wait() // 打印什么
zhangsan.waitAgain() // 打印什么
// this 场景题 - 2
class People {
constructor(name) {
this.name = name
this.age = 20
}
sayHi() {
console.log(this)
}
}
const zhangsan = new People('张三')
zhangsan.sayHi() // 打印什么
this的指向不用特意去死记硬背 抓准核心点:看调用对象
题目解答
this 的不同应用场景
- 作为普通函数调用 -> 一般指向函数调用者
- 使用
call
apply
bind
-> 改变this指向 - 作为对象方法调用 -> 一般指向调用对象
- 在 class 的方法中调用 -> 一般指向实例
- 箭头函数 -> 指向箭头函数定义作用域
创建 10 个<a>
标签,点击的时候弹出来对应的序号
// 创建 10 个`<a>`标签,点击的时候弹出来对应的序号
let i, a
for (i = 0; i < 10; i++) {
a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function (e) {
e.preventDefault()
alert(i)
})
document.body.appendChild(a)
}
经典作用域问题,解决方案:
- 使用let定义i 形成局部作用域 限制自由变量的查找层级
- 通过闭包
实际开发中闭包的应用
参考之前的示例。再次强调闭包中自由变量的寻找方式!!!
// 隐藏数据,只提供 API
function createCache() {
let data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
let c = createCache()
c.set('a', 100)
console.log( c.get('a') )
手写 bind 函数
先回顾 bind 函数的应用
function fn1(a, b) {
console.log('this', this)
console.log(a, b)
}
const fn2 = fn1.bind({ x: 100 }, 10, 20) // bind 返回一个函数,并会绑定上 this
fn2()
手写 bind
Function.prototype.bind1 = function () {
// 将参数解析为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this(取出数组第一项,数组剩余的就是传递的参数)
const t = args.shift()
const self = this // 当前函数
// 返回一个函数
return function () {
// 执行原函数,并返回结果
return self.apply(t, args)
}
}
4. 异步
异步本章节就不再赘述了,上篇异步进阶已经讲解。