# js-基础知识
# 类型
JS 数据类型分为两大类:
- 原始类型
- 对象类型
# 原始(Primitive)类型
在 JS 中,存在着 7 种原始值,分别是:
boolean
null
undefined
number
string
symbol
bigint
# 对象(Object)类型
对象类型和原始类型不同的是,原始类型存储的是值,一般存储在栈上,对象类型存储的是地址(指针),数据存储在堆上
当创建了一个对象类型的时候,计算机会在堆内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
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 的值也被修改了,也就是说 age 从 25 变成了 26 但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图
所以最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,导致了最终两个变量的值是不相同的。
# 值类型vs引用类型
# 值类型 保存在栈中 保存与复制的是值本身
let a = 100; let b= a; a=200;
console.log(b) --> 100
# 引用类型 保存在堆中 保存与复制的是指向对象的一个指针
引用数据改变 原来的数据改变

var a = {name:"percy"};
var b;
b = a;
a.name = "zyj";
console.log(b.name); // zyj
b.age = 22;
console.log(a.age); // 22
var c = {
name: "zyj",
age: 22
};
# 类型判断
类型判断有多种方式。
# typeof
typeof 对于原始类型来说,除了 null 都可以显示正确的类型,如果你想判断 null 的话可以使用 variable === null。
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof 1n // bigint
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型。
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
# instanceof
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 来判断类型是不行的,但是我们还是有办法实现的。
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 了。
这其实也侧面反映了一个问题:instanceof 并不是百分之百可信的。
另外其实我们还可以直接通过构建函数来判断类型:
// true
[].constructor === Array
# Object.prototype.toString.call
前几种方式或多或少都存在一些缺陷,Object.prototype.toString.call 综合来看是最佳选择,能判断的类型最完整,基本上是开源库选择最多的方式。
Object.prototype.toString.call(obj) === `[object ${type}]`;
# isXXX API
同时还存在一些判断特定类型的 API,选了两个常见的:
Array.isArray()
# this
this 是很多人会混淆的概念,但是其实它一点都不难,只是网上很多文章把简单的东西说复杂了。在这一小节中,你一定会彻底明白 this 这个概念的。
我们先来看几个函数调用的场景
function foo() {
console.log(this.a)
}
var a = 1
foo()
const obj = {
a: 2,
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: 'cxk' }
function foo() {
console.log(this.name)
}
foo.bind(a)() // => 'cxk'
以上就是 this 的所有规则了。实际中可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。
首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
如果你还是觉得有点绕,那么就看以下的这张流程图吧,图中的流程只针对于单个规则。
# 常见面试题
这里一般都是考 this 的指向问题,牢记上述的几个规则就够用了,比如下面这道题:
const a = {
b: 2,
foo: function () { console.log(this.b) }
}
function b(foo) {
// 输出什么?
foo()
}
b(a.foo)
# 手写深拷贝
- obj 是 null ,或者不是对象和数组,直接返回
- 判断obj 是数据或者现象 初始值
- 遍历数组 递归赋值
- 返回结果值
/**
* 深拷贝
*/
const obj1 = {
age: 20,
name: 'xxx',
address: {
city: 'beijing',
},
arr: ['a', 'b', 'c'],
}
const obj2 = deepClone(obj1)
obj2.address.city = 'shanghai'
obj2.arr[0] = 'a1'
console.log(obj1.address.city)
console.log(obj1.arr[0])
// beijing
// a
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
// obj 是 null ,或者不是对象和数组,直接返回
return obj
}
// 初始化返回结果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用!!!
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
# 浅拷贝
首先可以通过 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
# 类型转换
首先我们要知道,在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
# 闭包
假如一个函数能访问外部的变量,那么就形成了一个闭包,而不是一定要返回一个函数。
let a = 1
// 产生闭包
function fn() {
console.log(a);
}
function fn1() {
let a = 1
// 产生闭包
return () => {
console.log(a);
}
}
const fn2 = fn1()
fn2()
所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!!!
- 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a)
}
}
const fn = create()
const a = 200
fn() // 100
- 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}
const a = 100
function fn() {
console.log(a)
}
print(fn) // 100
# 闭包现实的使用场景
- 封装函数 提供api
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
},
}
}
const c = createCache()
c.set('a', 100)
console.log(c.get('a'))
- 作用域
let a
for (let 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 a,i;
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
- 将参数拆解为数组
- 获取 this(数组第一项)
- 改变this 的指向 返回一个函数
// 模拟 bind
Function.prototype.bind1 = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this(数组第一项)
const t = args.shift()
// fn1.bind(...) 中的 fn1
const self = this
// 返回一个函数
return function () {
return self.apply(t, args)
}
}
function fn1(a, b, c) {
console.log('this', this)
console.log(a, b, c)
return 'this is fn1'
}
const fn2 = fn1.bind1({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)
# 作用域(scope)
JavaScript 中的作用域是一种机制,它决定代码片段对其他部分的可访问性。并且回答了以下问题: 从哪里可以访问?从哪里无法进入?谁可以访问它,谁不能? 简单来说,作用域就是规定变量与函数可访问范围的一套规则。
# 有哪些作用域
- 全局作用域
window.name
window.location
window.top
- 函数作用域
let a = "hello";
function greet() {
let b = "World"
console.log(a + b);
}
greet();
console.log(a + b); // error
在上面的程序中,变量 a 是全局变量,变量 b 是局部变量。变量 b 只能在函数 hello 中访问。
- 块级作用域
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
块级作用域由最近的一对包含花括号{} 界定。换句话说, if 块、while 块、function块,单独的块块级作用域。
# 作用域链(scope chain)
蓝色框是全局作用域,定义了a变量,以及里面的所有函数。
红色框是first函数的作用域,它定义了变量 b ,以及second函数。
绿色框是第second的作用域。log语句用于输出变量 a、 b 和 c。
当代码执行道second函数是,打印变量a,b,c,但是变量 a 和 b 没有在second函数中定义,只定义了c。这时就会往上层作用域查找,于是从first函数找到了变量 b = 'Hello'。这时还没用找到a,所有再继续往上层作用域查找,然后找到了a = 'Hey',这样一层一层往上查找的过程,就被成为作用域链。 当 JS 引擎无法在作用域链中找到变量时,它就会停止执行并抛出错误。
# 原型链 与 原型
// 父类
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} 教授 ${this.major}`)
}
}
// 实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
xialuo.eat()
// 实例
const wanglaoshi = new Teacher('王老师', '语文')
console.log(wanglaoshi.name)
console.log(wanglaoshi.major)
wanglaoshi.teach()
wanglaoshi.eat()
# 一、原型
JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身
下面举个例子:
函数可以有属性。 每个函数都有一个特殊的属性叫作原型prototype
function doSomething(){}
console.log( doSomething.prototype );
控制台输出
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
上面这个对象,就是大家常说的原型对象
可以看到,原型对象有一个自有属性constructor,这个属性指向该函数,如下图关系展示

# 二、原型链
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法
在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法
下面举个例子:
function Person(name) {
this.name = name;
this.age = 18;
this.sayName = function() {
console.log(this.name);
}
}
// 第二步 创建实例
var person = new Person('person')
根据代码,我们可以得到下图

下面分析一下:
构造函数Person存在原型对象Person.prototype
构造函数生成实例对象person,person的__proto__指向构造函数Person原型对象
Person.prototype.proto 指向内置对象,因为 Person.prototype 是个对象,默认是由 Object函数作为类创建的,而 Object.prototype 为内置对象
Person.proto 指向内置匿名函数 anonymous,因为 Person 是个函数对象,默认由 Function 作为类创建
Function.prototype 和 Function.__proto__同时指向内置匿名函数 anonymous,这样原型链的终点就是 null
# 三、总结
下面首先要看几个概念:
__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的
每个对象的__proto__都是指向它的构造函数的原型对象prototype的
person1.proto === Person.prototype 构造函数是一个函数对象,是通过 Function构造器产生的
Person.proto === Function.prototype 原型对象本身是一个普通对象,而普通对象的构造函数都是Object
Person.prototype.proto === Object.prototype 刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function构造产生的
Object.proto === Function.prototype Object的原型对象也有__proto__属性指向null,null是原型链的顶端
Object.prototype.proto === null 下面作出总结:
一切对象都是继承自Object对象,Object 对象直接继承根源对象null
一切的函数对象(包括 Object 对象),都是继承自 Function 对象
Object 对象直接继承自 Function 对象
Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象
# 实例演示:
function Person(name) {
this.name = name
this.age = 18
this.sayName = function () {
console.log(this.name)
}
}
// 第二步 创建实例
var person = new Person('person')
// 实例(即person)的__proto__和原型对象指向同一个地方**
// 原型对象(即Person.prototype)的constructor指向构造函数本身**
console.log(person.__proto__ == Person.prototype)
console.log(Person.prototype.constructor == Person)
var obj = new Object()
console.log(obj.__proto__ == Object.prototype)
// 除了Object的原型对象(Object.prototype)的__proto__指向null,其他内置函数对象的原型对象(例如:Array.prototype)和自定义构造函数的__proto__都指向Object.prototype, 因为原型对象本身是普通对象。
console.log(Object.prototype.__proto__ == null)
console.log(Person.__proto__ == Function.prototype)
console.log(Function.prototype.__proto__ == Object.prototype)
// Object 对象直接继承自 Function 对象
console.log(Object.__proto__ == Function.prototype)
# 防抖与节流
# 定义
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
# 防抖
设计思路:事件触发后开启一个定时器,如果事件在这个定时器限定的时间内再次触发,则清除定时器,在写一个定时器,定时时间到则触发。
在防抖函数中,我们使用了闭包来保存定时器变量 timer 和传入的函数 func。每次触发事件时,我们先清除之前的定时器,再设置一个新的定时器。如果在 delay 时间内再次触发事件,就会清除之前的定时器并设置一个新的定时器,直到 delay 时间内不再触发事件,定时器到达时间后执行传入的函数 func。
function debounce(fn, delay){
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(()=> {
fn.apply(this, arguments);
}, delay)
}
}
# 节流
设计思路:我们可以设计一种类似控制阀门一样定期开放的函数,事件触发时让函数执行一次,然后关闭这个阀门,过了一段时间后再将这个阀门打开,再次触发事件。
刚开始valid为true,然后将valid重置为false,进入了定时器,在定时器的时间期限之后,才会将valid重置为true,valid为true之后,之后的点击才会生效
在定时器的时间期限内,valid还没有重置为true,会一直进入return,就实现了在N秒内多次点击只会执行一次的效果
function throttle(fn, delay){
let valid = true;
return function(){
if(valid) { //如果阀门已经打开,就继续往下
setTimeout(()=> {
fn.apply(this, arguments);//定时器结束后执行
valid = true;//执行完成后打开阀门
}, delay)
valid = false;//关闭阀门
}
}
}
# 防抖和节流区别:
防抖是触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。适用于可以多次触发但触发只生效最后一次的场景。
节流是高频事件触发,但在n秒内只会执行一次,如果n秒内触发多次函数,只有一次生效,节流会稀释函数的执行频率。