前端面试题之JS相关

184 阅读7分钟

JavaScript

原型与原型链 prototype和__proto__

写在前面

我听过一种说法叫:原型prototype__proto__是函数特有的。这句话倒也没错,但他举的例子有些问题。

function fn(){}
console.log(fn.prototype) // 此处可以正常输出,代表拥有prototype这个属性

const arr = []
console.log(arr.prototype) // 此处只会输出undefined,代表不拥有这个属性

以上是举例,确实没有任何错误,从代码上来说。

所以得出结论:数组和对象是没有原型的。

从证明过程上来说,我不认同这一点。但从结果上来说,我是认同的。

原型

现在,我开始解释为什么我觉得上面的例子有些问题。

先给一段常见的代码来展示一下

// 如果我想在项目中所有的数组变量中嵌入某个自定义方法
// 那我是不是该这么写,对象同理
Array.prototype.myfunc=function(){
    console.log('myfunc')
}

// 定义对象或者数组 常见版
const obj = {}
const arr = []
// 陌生版 其实效果都一样
const obj = new Object()
const arr = new Array()

根据这段代码,很明显能知道:哦!原来我们常用的ArrayObject全局变量是个函数啊。

那么再看上面的那个例子,仔细看是不是对比级别有些问题

function fn(){}
const f1 = new fn()
const obj = new Object()
console.log(f1.prototype) // undefined
console.log(obj.prototype) // undefined

console.log(fn.prototype) // 有对应原型的输出
console.log(Object.prototype) // 有对应原型的输出

所以要实例与实例对比,原型与原型对比。这才有可比性。悄咪咪说一句,其实StringNumber也是一样的。

原型的补充 关于Function

后面,我也尝试了使用Function去定义函数,还挺好玩的

const func = new Function ([arg1, arg2, ...argN], functionBody);
const sum = new Function('a', 'b', 'return a + b')
const hello = new Function('console.log("hello world")')

再换个角度一想,new Function出来的何尝也不是实例,虽然一般也见不着这种,但他确实也有prototype

可能其他数据类型单独把原型抽出来的原因是怕开发人员通过变量直接对数据类型的自带方法或属性做修改吧。

原型链

function fn(){
  console.log('我被执行啦')
}
const func = new fn()
console.log(func.num) // undefined
fn.prototype.num = 1
console.log(func.num) // 1
func.num = 2
console.log(func.num) // 2

从上面代码可以看得出来,func中本来没有num属性;但通过对原型上添加属性的方式,func就有了num属性。而对func直接添加同名属性时,num变成了func直接添加进去的属性值。

那么这就是原型链的两个特征

  • 就近原则 当访问某个变量下的属性时,会优先使用离他最近的同名属性
  • 传递原则 当这一层没有该属性时,会顺着原型链__proto__一层一层找下去,直到null原型链的最底层

判断原型的方式

Object.prototype.toString.call()

可以判断所有的数据类型,会返回一个字符串。

> Object.prototype.toString.call([])
'[object Array]'
> Object.prototype.toString.call({})
'[object Object]'
> Object.prototype.toString.call(function (){})
'[object Function]'
> Object.prototype.toString.call(1)
'[object Number]'
> Object.prototype.toString.call('1')
'[object String]'
> Object.prototype.toString.call(Symbol())
'[object Symbol]'
> Object.prototype.toString.call(null)
'[object Null]'
> Object.prototype.toString.call(undefined)
'[object Undefined]'
> Object.prototype.toString.call(true)
'[object Boolean]'

instanceof

用来判断某个变量的原型链是否存在某个原型(注意整条原型链的原型)

> [] instanceof Object
true
> [] instanceof Array
true

typeof

一般只用来判断基本数据类型

> typeof []
'object'
> typeof {}
'object'
> typeof function(){}
'function'
> typeof null
'object'
> typeof Symbol()
'symbol'
> typeof 1
'number'
> typeof '1'
'string'
> typeof undefined
'undefined'
> typeof true
'boolean'

可以看得出来,它并不能很好地判断引用数据类型

闭包

一文颠覆大众对闭包的认知 - 掘金 (juejin.cn)

防抖与节流

防抖:某个函数在规定时间内,只使用最后一次触发的参数执行;简单来说:将多次操作只保留最后一次。

function debounce(func, wait, immediate) {
  let timeout
  return function () {
    const context = this
    const args = [...arguments]
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      timeout = null
      !immediate && func.apply(context, args)
    }, wait)
    immediate &&  func.apply(context, args)
  };
}

使用场景:输入框联想功能调用接口、多频率点击事件等

节流:某个函数在规定时间内,只调用第一次

function throttle(fn, wait) {
  let pre = 0;
  return function (...args) {
    let now = Date.now();
    if (now - pre >= wait) {
      fn.apply(this, args);
      pre = now;
    }
  };
}

使用场景:轮播图左右按钮、提交表单(防抖也可以,不过一般建议用loading覆盖操作层)

PS:两种皆为闭包使用场景

实现once函数 传入函数只执行一次

function once(func) {
  let flag = true;
  return function () {
    if(flag){
      flag = false
      func.apply(null,arguments)
    }
  }
}

JavaScript延迟加载的方式

JavaScript 会阻塞 DOM 的解析,因此也就会阻塞 DOM 的加载。所以有时候我们希望延迟 JS 的加载来提高页面的加载速度。

  • script 标签的defer属性 脚本会立即下载但延迟到整个页面加载完毕再执行。该属性对于内联脚本无作用。
  • script 标签的Async属性 是在外部 JS 加载完成后,浏览器空闲时,Load 事件触发前执行,标记为async的脚本并不保证按照指定他们的先后顺序执行,该属性对于内联脚本无作用。
  • 动态创建script标签,监听dom加载完毕再引入js文件

图片的懒加载和预加载

  • 预加载 提前加载图片,当用户需要查看时可直接从本地缓存中渲染
  • 懒加载 减少请求或延迟请求

实现休眠sleep函数

据说有人以前用这个方法来一钱多赚,美其名曰提升性能?(笑)

promise

function sleep(ms) {
  const pause = new Promise((resolve)=>{
    setTimeout(resolve,ms)
  })
  return pause
}

sleep(1000).then()

promise + async

function sleep(ms) {
  const pause = new Promise((resolve)=>{
    setTimeout(resolve,ms)
  })
  return pause
}

async function testSleep() {
  await sleep(1000)
}

new 操作符

  1. 创建一个新对象,即{}
  2. 让这个对象的[[Prototype]](_proto_)属性指向构造函数的prototype
  3. 构造函数内部的this被绑定到这个对象
  4. 如果构造函数返回值是一个对象,那么就会返回这个对象。否则返回创建的对象。
// fn构造函数
function MyNew(fn){
    // 判断fn是否为函数 
    if(typeof fn !== "function")
        throw Error(fn+"is not a function");
    // 生成一个新的对象
    let newObj = {};
    // 让这个对象的`[[Prototype]]`属性指向构造函数的`prototype`
    newObj.__proto__ = fn.prototype;
    // 以上两步可以用这个代替
    // const newObj = Object.create(fn.prototype);
    // 获取传入构造参数
    let arg = Array.prototype.slice.call(arguments,1);
    // 绑定this,并执行构造函数
    let res = fn.apply(newObj,arg);
    // 判断返回值是否是对象
    return res instanceof Object?res:newObj;
}

JavaScript中this的五种情况

  1. 作为普通函数执行时,this指向window
  2. 当函数作为对象的方法被调用时,this就会指向该对象。
  3. 使用new操作符调用,会返回新的对象或方法本身具有返回引用数据类型的变量
  4. 箭头函数,this指向取决于定义时的上级作用域的最近一级的对象。
  5. apply、call和bind会修改方法执行时的this指向,区别在于bind并不会立即执行,只是返回一个函数。而其他两个都会立刻执行。

跨域方案

CORS:服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求,前后端都需要设置。

代理跨域:启一个代理服务器,实现数据的转发

JS隐式转换

一文让你搞懂JavaScript隐式转换 - 掘金 (juejin.cn)

普通函数和箭头函数的区别

1、普通函数的this与调用对象相关,而箭头函数的this指向由定义时的上一级确认,且无法改变 2、普通函数有arguments,而箭头函数没有 3、普通函数可以用new调用,箭头函数不可以使用new关键字 4、普通函数有原型链,而箭头函数没有

JS垃圾回收机制

一文让你彻底搞懂JS垃圾回收机制 - 掘金 (juejin.cn)

以下为埋坑问题,目前懒得写,又怕以后忘记,所以先写个标题

  • 重绘与回流 也许该写个CSS相关的了
  • EventLoop 事件循环 浏览器环境与node环境 (好像在ES相关引用了一篇其他人的文章)不想写了
  • setTimeout、Promise、Async/Await的区别
  • 事件捕获、冒泡和委托
  • 深拷贝
  • js延迟加载
  • call、apply 、bind