1.javascript 有几种数据类型?
javascript 有八种数据类型
其中基本数据类型有
- undefined
- null
- boolean
- string
- number
- symbol (es6)
- bigint (es10)
引用数据类型有 Object (Function,Array,...)
2.请简单介绍一下基本数据类型和引用数据类型的区别?以及讲下如何判断数据的类型?
基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问
引用类型是存放在堆内存中的对象,变量其实是保存的在栈内存中的一个指针,这个指针指向堆内存。
所以以下代码是 OK 的
const arr = []
arr.push(1)
console.log(arr) //[1]
- typeof 只能判断除了 null 以外的基本数据类型以及函数
typeof 'a'; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof Symbol(); // symbol 有效 ---注意Symbol是基本数据类型,不能使用new关键字
typeof undefined; //undefined 有效
typeof null的问题
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。
- instanceof 判断原理:判断该对象是否在指定类型的原型链上
手写一个 instanceof
function myinstanceOf(son, father) {
if (typeof son !== "object") {
return false
}
let sonPro = Reflect.getPrototypeOf(son);
let fatherPro = father.prototype;
while (sonPro) {
if (sonPro === fatherPro) {
return true
}
sonPro = Reflect.getPrototypeOf(sonPro);
}
return false
}
//测试
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);
console.log(auto instanceof Car); //true
console.log(auto instanceof Object); //true
console.log(myinstanceOf(auto, Car)) //true
console.log(myinstanceOf(auto, Object)) //true
一般用于判断引用类型
[] instanceof Array; // true
new Date() instanceof Date;// true
- toString (建议使用)
一般使用 String 对象原型上的 toString 方法,原因是对象可能会重写 toString
封装自己的 type 方法
let type = function (o) {
let s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
['Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
};
});
3.new 一个对象时,都做了什么?
做了以下四件事
1.创建一个空对象,作为将要返回的对象实例。
2.将这个空对象的原型,指向构造函数的 prototype 属性。
3.将这个空对象赋值给函数内部的 this 关键字。
4.开始执行构造函数内部的代码。
按照以上说明,手动实现一个 new 方法
如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象
function New(...arg) {
const obj = {}; //创建一个空对象 --对应1
const Constructor = arg.shift(); //获取第一个参数,即构造函数,同时删除第一个参数
obj.__proto__ = Constructor.prototype; //将这个空对象的原型,指向构造函数的prototype属性 ----对应2
const res = Constructor.apply(obj, arg); //执行构造函数方法,将方法里的this指向创建的obj对象 ---- 对应 3,4
return res instanceof Object ? res : obj //方法有返回值则返回,没有则返回新创建的对象
}
//测试
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);
console.log(auto) //Car{make: "Honda", model: "Accord", year: 1998}
const auto2 = New(Car, 'Honda', 'Accord', 1998)
console.log(auto2) //Car{make: "Honda", model: "Accord", year: 1998}
4.说一说 js 中的对象继承?
js 中的继承指得是
1.继承目标对象的实例方法
2.继承原型上的方法
//es5
function Parent(name) {
this.name = name;
this.sayHi = function () {
console.log("Hi" + this.name + ".");
}
}
function Children(name) {
Parent.apply(this, name) // 实现----1
this.getName = function () {
console.log(this.name);
}
}
Children.prototype = Object.create(Parent.prototype) //实现-----2
Children.prototype.constructor = Children
//Object.create原理解析
function objectcreate(prototype) {
function F() { }
F.prototype = prototype
return new F()
}
//es6
class Parent {
constructor(name) {
this.name = name;
}
sayHi() {
console.log("Hi" + this.name + ".");
}
};
class Children extends Parent { //extends关键字 实现---2
constructor(name) {
super(name) //super关键字实现---1,注意super必须放在构造函数顶部调用
}
}
5.聊一下 js 中的 this,并讲一下改变 this 指向的方式有哪些?箭头函数有自己的 this 吗?
面向对象语言中 this 表示当前对象的一个引用
但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。
个人理解是 this 指向使用 this 关键字所在作用域的那个对象,也是大家常说的谁调用,就指向谁
如下:
let obj = {
say() {
console.log(this)
}
}
obj.say() //输出obj 此时的this在obj环境下
let fn = obj.say
fn() //输出window
let obj2 = {
obj2obj2: {
say: obj.say
},
say: obj.say
}
obj2.obj2obj2.say() //输出obj2obj2
obj2.say() //输出obj2
构造函数中的 this 有所区别,原因是 new 关键字改变了 this 指向,可以参考上面
js 中提供了三种方法可以改变 this 的指向
- call 方法
function.call(thisArg, arg1, arg2, ...) 第一个参数为函数方法 this 所指向的对象,剩余参数为函数的参数
调用此方法后,函数会立即执行
实现一个 call 方法
Function.prototype.myCall = function (context, ...arg) {
context = (context === null || context === undefined) ? window : Object(context)
let fnKey = Symbol()
context[fnKey] = this //this表示要运行的函数,赋值给键名
let res = context[fnKey](...arg) //调用方法
// delete context[fnKey] //删除键名
Reflect.defineProperty(fnKey) //es6规范
return res
}
- apply 方法
func.apply(thisArg, [argsArray]) 与 call 方法不同的是,此方法的第二个参数为一个数组
实现一个 apply 方法
Function.prototype.myApply = function (context, arg) {
context = (context === null || context === undefined) ? window : Object(context)
let fnKey = Symbol()
context[fnKey] = this
let res;
if (arg) {
res = context[fnKey](...arg)
} else {
res = context[fnKey]()
}
Reflect.defineProperty(fnKey)
return res
}
- bind 方法
function.bind(thisArg[, arg1[, arg2[, ...]]]),第二个参数为参数列表 此方法不会立即执行,而是返回一个方法
实现一个 bind 方法
Function.prototype.mybind = function (context, ...arg) {
context = (context === null || context === undefined) ? window : Object(context)
var thatFunc = this
return function () {
return thatFunc.apply(context, arg.concat(...arguments));
}
}
箭头函数()=>{}
箭头函数没有自己的 this,而是继承父级作用域的 this,
没有自己的 this,当然也不可以使用 call,apply,bind 以及 new 方法
6.讲一下防抖节流,以及适用场景?
1.节流函数
听名字我们也大概知晓什么意思,意思是创建并返回一个像节流阀一样的函数,当重复调用函数的时候,最多每隔 wait 毫秒调用一次该函数
听了上面的解析,发现有一个问题,规定时间内执行,到底是最开始就执行一次,间隔 wait 秒在执行,还是说 wait 秒中之后执行,再 wait 秒中之后执行
1.1 在函数调用前先执行
思路如下:
利用闭包,创建一个变量,用来保存上次执行的时间,然后在函数中判断,如果时间间隔大于 wait 秒,执行函数,否则不作为
/**
* @param func 执行函数
* @param wait 时间间隔
* @returns {Function}
*/
let throttle = function (func, delay) {
let prev = 0;
return function () {
let context = this;
let args = arguments;
let now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
适用场景:防止用户重复提交,如表单提交,发送短信,输入次数过多等
1.2 在时间间隔之后执行方法
思路如下:
使用定时器,如果定时器不存在,则创建定时器,定时器中执行方法,同时清除定时器 id
let throttle = function (func, delay) {
let timer = null;
return function () {
let context = this;
let args = arguments;
if (!timer) {
timer = setTimeout(function () {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
适用场景:滚动条事件,搜索框输入发送 ajax,输入框判断是否合法等
1.3 让函数前时间间隔前后都执行
采用时间戳加定时器的形式
let throttle = function (func, delay) {
let timer = null;
let startTime = Date.now(); //设置开始时间
return function () {
let curTime = Date.now();
let remaining = delay - (curTime - startTime); //剩余时间
let context = this;
let args = arguments;
if (remaining <= 0) { // 间隔大于或者等于delay秒
func.apply(context, args);
startTime = Date.now();
} else { // 间隔小于或者等于delay秒
clearTimeout(timer);
timer = setTimeout(func, remaining); //取消当前计数器并计算新的remaining
}
}
}
remaining 为两次方法执行间隔与 delay 的差距,为负数则表示,间隔时间大于 delay 秒,反之小于 delay 秒
1.4 扩展,结合上面三个方法,动态使用
使用第三个参数 options,以对象形式,可以传入两个参数 leading,trailing
/**
* 创建并返回一个像节流阀一样的函数,当重复调用函数的时候,最多每隔 wait毫秒调用一次该函数
* @param func 执行函数
* @param wait 时间间隔
* @param options 如果你想禁用第一次首先执行的话,传递{leading: false},
* 如果你想禁用最后一次执行的话,传递{trailing: false}
* @returns {Function}
*/
let throttle = function (func, wait, options) {
let context, args, result; //this上下文,参数,返回值
let timeout = null; //定时器id
let previous = 0; //上次执行的时间
if (!options) options = {}; //禁用首次或者末次执行
let later = function () { //remaining秒之后执行方法
//初始化函数
previous = options.leading === false ? 0 : new Date().getTime(); //没有禁用第一次执行,则previous=0
timeout = null; //初始化定时器id
result = func.apply(context, args); //执行方法
context = args = null;
};
return function () {
let now = new Date().getTime(); //获取当前时间
if (!previous && options.leading === false) {
//当第一次执行时,如果禁用第一次执行(options.leading),将本次执行时间赋值给上次执行
previous = now
}
let remaining = wait - (now - previous); //用时间间隔wait减去(本次运行的时间now-上次运行的时间previous),使时间间隔为wait
context = this; //获取当前环境this
args = arguments; //获取函数参数
if (remaining <= 0 || remaining > wait) { //如果两次方法执行时间间隔刚好为wait,或者大于wait,
previous = now; //将本次时间赋值给上次
result = func.apply(context, args); //执行方法
context = args = null
} else if (!timeout && options.trailing !== false) {
// 时间间隔小于wait,且定时器不存在,且最后一次执行
timeout = setTimeout(later, remaining); //在remaining时间后执行方法,remaining为距离wait所剩余的时间
}
return result;
};
}
原理是根据变量初始化 previous 变量,和判断是否生成定时器
2.防抖函数
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
这个和节流不同的地方是,这个函数如果一直触发,则一直不会执行,只会在不触发的 wait 秒后执行一次
在函数停止执行的 wait 秒后执行
let debounce = function (fn, wait) {
let timeout = null;
return function () {
if (timeout !== null) clearTimeout(timeout);
timeout = setTimeout(fn, wait);
}
}
原理:使用定时器,每次运行时判断定时器是否存在,存在则清除定时器,在生成一个 wait 秒后执行的定时器
扩展,自定义函数首次执行还是末次执行
/**
* 防反跳。func函数在最后一次调用时刻的wait毫秒之后执行!
* @param func 执行函数
* @param wait 时间间隔
* @param options 如果你想第一次首先执行的话,传递{leading: true},
* 如果你想最后一次执行的话,传递{trailing: true}
* @returns {Function}
*/
let debounce = function (func, wait, options) {
let timeout, args, context, timestamp, result;
let later = function () {
let last = new Date().getTime() - timestamp; // timestamp会实时更新
if (last < wait && last >= 0) { //如果定时器执行时间距离上次函数调用时间,大于0,小于wait,则重新生成定时器,时间间隔为距离wait所剩余的时间
timeout = setTimeout(later, wait - last);
} else {
timeout = null; //清除定时器
if (options.trailing) { //如果定义时间间隔后执行
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function () {
context = this; //绑定函数上下文
args = arguments;
timestamp = new Date().getTime(); //当前执行方法时间
let callNow = options.leading && !timeout;
if (!timeout) {
//生成定时器
timeout = setTimeout(later, wait);
}
if (callNow) { //如果定义首次执行
//立即执行
result = func.apply(context, args);
context = args = null;
}
return result;
};
}
原理:执行时判断定时器,如果定时器存在,则不作为,然后在定时器执行方法中判断时间间隔,大于 0 小于 wait,则生成新的定时器,每次执行方法动态更新 timestamp 的值。
7.讲一下 js 中的作用域链,原型链?
js 中的作用域分为两种,函数作用域和全局作用域,es6 新增了块级作用域,使用 let 和 const 关键字
每个函数都有自己的作用域,函数中嵌套有函数,以此形成了作用域链,作用域链最顶端为全局作用域
var a = 20;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest(); //40
}
上面例子的作用域链为 innerTest()--->test()----->window 环境(浏览器模式下)
在作用域中找不到的变量,就会沿着作用域链往上找,一直找到顶端
如下 innerTest 函数中的变量 b,是取得 test()函数作用域中的变量
原型链
原型链是 js 中对象的一种特殊指针,可以解决一些方法公用和继承的问题
每个对象都有自己的原型,而原型对象又有自己的原型,因此形成一种链式结构,原型链的最顶端为 Object 对象
8.讲一下 js 中的事件循环?
javascript 是单线程的语言,也就是说,同一个时间只能做一件事。
而这个单线程的特性,与它的用途有关,作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。
这决定了它只能是单线程,否则会带来很复杂的同步问题。
比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
说起事件循环,得说一下 js 中的同步任务和异步任务
异步任务中又分宏任务和微任务
除了广义的同步任务和异步任务,JavaScript 单线程中的任务可以细分为宏任务和微任务。
宏任务包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
微任务包括:process.nextTick, Promises.then, Object.observe, MutationObserver。
js 执行机制为先执行同步任务,发现其中有异步任务,讲任务挂起,添加到下一轮任务队列中,当本轮的同步任务执行完成后,讲异步队列中的任务转为同步任务,重复这个过程,但是要注意的是,异步队列转为同步任务要先执行微任务,再执行宏任务
以上是个人的理解,如有不对的地方,欢迎指出!