数据类型
基础类型:String、Number、Boolean、Undefined、Null
引用类型: Array、Object
ES6新增:Symbol
类型判断
- typeof
typeof可以用来进行判断基础类型的数据
typeof 'abc' // string
typeof 123 // number
typeof true // boolean
typeof undefined // undefined
typeof null // object
- instanceof
instanceof检测某个对象是否属于另一个对象的实例. mdn上的介绍是:用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上
// 实现一个instanceof
function myInstanceof(left,right){
// 获取类型的原型
let prototype = right.prototype
// 获取对象的原型
left = left._proto_
while(true){
if(left ===null) return false;
if(prototype === left) {
return true
}
left = left._proto_
}
}
- Object.prototype.toString
console.log(Object.prototype.toString.call(123)); //[object Number]
console.log(Object.prototype.toString.call('123')); //[object String]
console.log(Object.prototype.toString.call(undefined)); //[object Undefined]
console.log(Object.prototype.toString.call(true)); //[object Boolean]
console.log(Object.prototype.toString.call({})); //[object Object]
console.log(Object.prototype.toString.call([])); //[object Array]
console.log(Object.prototype.toString.call(function(){})); //[object Function]
console.log(Object.prototype.toString.call(null)); //[[object Null]]
类型转换
显示转换
Number()转换
原始类型转换成Number类型的规则:
- 数值:转换后还是数值
- 字符串: 可以解析为对应的数值,则转换成对应的数值,否则,转换为NaN, 特别需要注意空字符串是转换成0
- undefined: 转换成NaN
- null: 转换成0
- 布尔: true转换成1,false转换成0
- Symbol: 报错
引用类型转换成Number类型的规则:
转换的目标是对象,先调用对象的valueOf方法,如果返回的是原始类型的值,那么按照上述规则,进行类型转换。如果返回的是复合类型,那么调用该对象的toString方法,如果toString返回的是原始值类型,则按照上述规则转换,否则报错
Number({a:1})
// valueOf() -> {a:1} -> toString() -> '[object Object]' -> Number('[object Object]') -> NaN
Number([1,2])
// valueOf -> [1,2] -> toString() -> '1,2' -> Number('1,2') -> NaN
Number([1])
// valueOf -> [1] -> toString() -> '1' -> Number('1') -> 1
Number([])
// valueOf() -> [] -> toString -> '' -> Number('') -> 0
String()转换
原始类型转换成String类型的规则:
- 数值:转换成数值字符串
- 字符串:字符串
- null : 'null'
- undefined: 'undefined'
- 布尔: ‘true’或 ‘false’
- Symbol: ‘Symbol’
引用类型转换成String类型的规则:
转换的目标是对象,首先调用对象的toString方法,如果返回的结果是个原始类型,则按照上述规则转换,如果返回的结果还是个对象类型,那么调用对象的valueOf方法,如果返回的是原始类型,按照上述规则转化,否则就报错
const a1 = {b:1}
a1.toString() // '[object Object]'
const a2 = [1,2,3]
a2.toString() // 1,2,3
const a3 = []
a3.toString() // ''
const a4 = function(){}
a4.toString() // 'function(){}'
Boolean()转换 除了undefined、null 、NaN、''、 0 其余的全部转成true
隐式转换
触发隐式转换的行为:四则运算、判断语句、native调用(console.log()和alert方法会内部调用string方法)
四则运算中除了加法(+),其余的运算符都会把非Number类型的转换为Number类型
+:作为算术运算符,会把其他类型通过Number方法转换为数值类型;当+两边有一边是字符串是,会被当做字符串连接符
1+2 // 3
1+'' // '1'
// 有字符串,其他类型转为字符串
1+false // 1
// 其他类型先转为number类型,再相加
1+null // 1
// 其他类型先转为number类型,再相加
1+undefined // NaN
// 其他类型先转为number类型,再相加
1+function(){} // '1function(){}'
// 引用类型先转换为值类型
1+[] // '1'
// 引用类型先转换为值类型
1+[1,2] // '11,2'
// 引用类型先转换为值类型
==:
- NaN不等于任何值
- String与Number进行比较时,String转换为Number
- Boolean与其他类型进行比较,Boolean转换为Number
- null 和undefined,相等
- 引用类型和值类型进行比较,引用类型先转换为值类型
- 引用类型和引用类型进行比较,判断是否指向同一个引用
[] == [] // false
// 引用类型比较,不指向同一个引用 false
[] == 0 // true
// [] 转换为值类型为'',调用Number('') -> 0 ,0 ==0 ,所以为true
![] == 0 // true
// ![] 转换为false,调用Number(false) -> 0 所以为true
![] == [] // true
// ![]转换为false,调用Number(false) -> 0,[]转化为值类型‘’,调用Number('') -> 0 ,所以为true
{}==!{} // false
// !{}转换成false,调用Number(false) ->0,{}转换为值类型'[object Object]' Number('[object Object]') -> NaN,所以为false
{} == {} // false
// 引用类型比较,不指向同一个引用为false
this
this指向什么,完全取决于什么地方以什么方式调用,而不是创建时
this的绑定规则:
- 默认绑定
function foo(){
var a = 1
console.log(this.a) // 10
}
var a = 10
foo()
典型的默认绑定,foo调用的位置前面没有任何前缀,我们可以看做是window.foo(),所以非严格模式下,this指向就是window,严格模式下是undefined
- 隐性绑定
function foo(){
console.log(this.a)
}
var obj = {
a:10,
foo:foo
}
foo(); // undefined
obj.foo() // 10
答案:undefined、10
foo()直接调用,跟默认绑定是相同的,就是window.foo() ,但是全局中没有定义a,所以是undefined
下面obj.foo()这样调用,是的foo执行的时候有了上下文对象,既obj,函数内的this默认绑定为上下文对象,等价于打印obj.a,输出10.
如果是链式的关系,比如xx.yy.obj.foo(),上下文取函数的直接上级,既紧挨着的那个,或者说对象链的最后一个.
- 显示绑定
使用 call、apply,这几个的作用都是改变函数的this指向,第一个参数都是设置this对象。
这两个函数的区别:
- call从第二个参数开始所有的参数都是原函数的参数
- apply只接受两个参数,且第二个参数必须是数组
const obj = {a:1}
function foo(){
console.log(this.a)
}
foo.call(obj) // 1
foo.apply(obj) // 1
-------
function foo(a,b){
console.log(a+b);
}
foo.call(null,'海洋','饼干'); // 海洋饼干 这里this指向不重要就写null了
foo.apply(null, ['海洋','饼干'] ); // 海洋饼干
除了call、apply之外,还可以使用bind来改变this指向。调用bind函数之后,后者不会立即执行,而是将一个值绑定到函数的this上,并将绑定好的函数返回
function foo(){
console.log(this.a)
}
const obj = {a : 10}
foo = foo.bind(obj)
foo() // 10
- new 绑定
function Fn(){
this.name = '听风是风';
};
let echo = new Fn();
闭包
能够访问其他函数内部变量的函数,被称为闭包
对上面这句话进行一番理解,简单来说,闭包就是函数内部定义的函数,被返回了出去并在外部被调用
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 这就形成了一个闭包
作用域
作用域是指程序中定义变量的区域,该位置决定了变量的生命周期,也就是变量或函数的可访问范围
new
new操作符可以帮助我们构建出一个实例,并且绑定this
模拟实现一个new
function MyNew(){
const obj = {} // 声明一个对象,也是最终返回的对象
// 取出参数中的第一个参数,就是我们要传入的构造函数,此外,shift会改变原数组,所以arguments会被去除对一个参数
Constructor = [].shift.call(arguments)
// 将obj的原型指向构造函数的原型对象
obj.__proto__ = Constructor.prototype
const ret = Constructor.apply(obj,arguments)
return typeof ret === 'object' ? ret :obj
}
call、apply、bind
- call、apply
call和apply都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内的this的指向。
- call、apply的区别
对于call和apply而言,作用完全一样,只是接受参数的方式有区别,例如:
const func = function(a,b){}
// 使用两者调用
func.call(this,a,b)
func.apply(this,[a,b])
以上this是想要指定的上下文,call需要把参数按顺序传递进去,apply则是把参数放在数组里
- bind
bind同样能改变函数体内的this,跟call和apply的区别在于,bind是返回绑定好this的对应的函数
模拟实现call apply apply
Function.prototype.call2 = function(context) {
context = context || window
context.fn = this
const args = [].slice.call(arguments,1)
const result = context.fn(...args)
delete context.fn
return result
}
Function.prototype.apply2 = function(context,args){
context = context || window
context.fn = this
const result = context.fn(...args)
delete context.fn
return result
}
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
原型
在javascript中,函数可以有属性。每个函数都有一个特殊的属性叫做原型(prototype)
原型链
javascript对象通过__proto__指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,既原型链
继承
最优的一种继承:寄生式组合继承
function Parent() {
this.name = 'parent'
}
function Child() {
this.age = 22
Parent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
Promise
- promise的作用
解决异步请求中的嵌套问题,可以更好的组织多层嵌套的异步回调,优雅的写法
模拟实现一个基础的promise
class MyPromise {
constructor(executor) {
// 初始化值
this.initData();
// 初始化this指向
this.initBind();
// promise有throw的话,就相对于执行reject
try {
executor(this.resolve, this.reject);
} catch (error) {
this.reject(error);
}
}
initData() {
// 终值
this.PromiseResult = null;
// 状态
this.PromiseState = 'pending';
// 保存成功回调
this.onFulfilledCallbacks = [];
// 保存失败回调
this.onRejectedCallbacks = [];
}
initBind() {
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
}
resolve(value) {
// 状态是不可变的,已经是reject了,就不允许执行下面的操作
if (this.PromiseState !== 'pending') return false;
this.PromiseResult = value;
this.PromiseState = 'fulfilled';
while (this.onFulfilledCallbacks.length) {
// 从成功的回调结果中获取第一个回调函数,执行它
this.onFulfilledCallbacks.shift()(this.PromiseResult);
}
}
reject(reason) {
// 状态是不可变的,已经是fulfilled了,就不允许执行下面的操作
if (this.PromiseState !== 'pending') return false;
this.PromiseResult = reason;
this.PromiseState = 'rejected';
while (this.onRejectedCallbacks.length) {
// 从成功的回调结果中获取第一个回调函数,执行它
this.onRejectedCallbacks.shift()(this.PromiseResult);
}
}
then(onFulfilled, onRejected) {
const thenPromise = new MyPromise((resolve, reject) => {
const resolvePromise = cb => {
setTimeout(() => {
try {
const x = cb(this.PromiseResult);
if (x === thenPromise) {
throw new Error('不能返回自身');
}
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
} catch (error) {
reject(error);
}
});
};
if (this.PromiseState === 'fulfilled') {
// 成功状态,执行第一个回调
resolvePromise(onFulfilled);
} else if (this.PromiseState === 'rejected') {
// 失败状态,执行第二个回调
resolvePromise(onRejected);
} else if (this.PromiseState === 'pending') {
// 如果是待定状态,暂时保存两个回调
this.onFulfilledCallbacks.push(onFulfilled.bind(this));
this.onRejectedCallbacks.push(onRejected.bind(this));
}
});
// then方法需要返回包装过的promise
return thenPromise;
}
all(promises) {
const result = [];
let count = 0;
return new MyPromise((resolve, reject) => {
const addData = (index, value) => {
result[index] = value;
count++;
if (count === promises.length) resolve(result);
};
promises.forEach((promise, index) => {
if (promise instanceof MyPromise) {
promise.then(res => {
addData(index, res);
}, err => reject(err));
} else {
addData(index, promise);
}
});
});
}
race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
if (promise instanceof MyPromise) {
promise.then(res => {
resolve(res);
}, err => reject(err));
} else {
resolve(promise);
}
});
});
}
}
事件循环
js可以分为宏任务和微任务
宏任务:
- script代码
- setTimeout
- setInterval
- setImmediate
- UI render
微任务:
- promise
- process.nextick
- async/await
- mutationObserver
事件循环通俗的解释就是,进入主线程,执行宏任务,然后是执行该宏任务产生的微任务,如果微任务执行的过程中又产生了微任务,则继续执行微任务,微任务执行完之后,再回到宏任务中进行下一轮循环。
举个例子:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 输出顺序 script start 、async2 end 、promise、script end、async1 end、promise1、promise2、setTimeout
节流防抖
防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于阈值,防抖的情况下只会调用一次,而节流会每隔一定时间调用函数
带有立即执行选项的防抖函数
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce(func,wait=50,immediate=true){
let timer,context,args;
// 延迟执行函数
const later = ()=>{
setTimeout(()=>{
// 延迟执行函数执行完毕,清除缓存的定时器
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用之前缓存的参数和上下文
if(!immediate){
func.apply(context,args)
context = args = null
}
},wait)
}
return function (...params) {
// 如果没有创建延迟执行函数,则创建一个
if(!timer) {
timer = later()
// 如果是立即执行,调用函数,否则缓存参数和上下文
if(immediate) {
func.apply(this,params)
} else {
context = this;
args = params
}
} else {
clearTimeout(timert)
timer = later()
}
}
}
function throttle(fn, wait) {
let prev = new Date();
return function() {
const args = arguments;
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
柯里化
柯里化就是将多个参数的函数转化成接收一个参数的函数
function curry(func){
return function curried(...args){
if(args.length>=func.length){
return func.apply(this,args)
}else {
return function(...args2){
return curried.apply(this,args.concat(args2))
}
}
}
}
深拷贝
function deepClone(target, map = new Map()) {
if (typeof target !== 'object' || target === null) {
return target
}
const res = Array.isArray(target) ? [] : {}
if (map.get(target)) {
return map.get(target)
}
map.set(target, res)
for (const key in target) {
res[key] = deepClone(target[key])
}
return res
}
数组扁平化
function flat(data) {
data.reduce((prev, cur) => {
return Array.isArray(cur) ? [...prev, ...flat(cur)] : [...prev, cur]
}, [])
}
Promise应用
// 实现一个红绿灯交换的案例
function red() {
console.log('red')
}
function green() {
console.log('green')
}
function yellow() {
console.log('yellow')
}
const light = function (timer, cb) {
return new Promise((resolve) => {
setTimeout(() => {
cb()
resolve()
}, timer)
})
}
const step = function () {
Promise.resolve().then(() => {
return light(3000, red)
}).then(() => {
return light(2000, red)
}).then(() => {
return light(1000, yellow)
}).then(() => {
return step()
})
}