面试js相关

浅拷贝 深拷贝

浅拷贝和深拷贝都只针对于像Object, Array这样的复杂对象,

区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制

  • 浅拷贝

    • 直接赋值
    let a = {age: 1}
    let b = a
    // 弊端:对象在赋值的过程中其实是复制了地址,从而导致改变了一方其他也都被改变的情况。
    
    • Object.assign()
    let a = {age: 1}
    let b = Object.assign({}, a)
    // Object.assign() 可以解决直接赋值出现的问题
    
    • 展开运算符...
    let a = {age: 1}
    let b = {...a}
    
  • 深拷贝

    • JSON.parse(JSON.stringify(obj))
    let a = {
      age: 1,
      person: {
        tall: 180
      }
    }
    let b = JSON.parse(JSON.stringify(a))
    

弊端

会忽略undefined 会忽略symbol 不能序列化函数 不能解决循环引用的对象 循环引用,也不能通过该方法实现深拷贝 在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

  • MessageChannel
// 有undefined + 循环引用
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
  f: undefined
}
obj.c = obj.b;
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

function deepCopy(obj) {
  return new Promise((resolve) => {
    const {port1, port2} = new MessageChannel()
    port2.onmessage = ev => resolve(ev.data)
    port1.postMessage(obj)
  })
}

deepCopy(obj).then((copy) => {           // 请记住`MessageChannel`是异步的这个前提!
  let copyObj = copy;
  console.log(copyObj, obj)
  console.log(copyObj == obj)
})

但拷贝有函数的对象时,还是会报错,这个时候就要考虑用 lodash 的深拷贝函数了

  • 拷贝函数
function checkType(any) {
  return Object.prototype.toString.call(any).slice(8, -1)
}
function clone(any){
  if(checkType(any) === 'Object') { // 拷贝对象
    let o = {};
    for(let key in any) {
      o[key] = clone(any[key])
    }
    return o;
  } else if(checkType(any) === 'Array') { // 拷贝数组
    var arr = []
    for(let i = 0,leng = any.length;i<leng;i++) {
      arr[i] = clone(any[i])
    }
    return arr;
  } else if(checkType(any) === 'Function') { // 拷贝函数
    return new Function('return '+any.toString()).call(this)
  } else if(checkType(any) === 'Date') { // 拷贝日期
    return new Date(any.valueOf())
  } else if(checkType(any) === 'RegExp') { // 拷贝正则
    return new RegExp(any)
  } else if(checkType(any) === 'Map') { // 拷贝Map 集合
    let m = new Map()
    any.forEach((v,k)=>{
      m.set(k, clone(v))
    })
    return m
  } else if(checkType(any) === 'Set') { // 拷贝Set 集合
    let s = new Set()
    for(let val of any.values()) {
      s.add(clone(val))
    }
    return s
  }
  return any;
}
// 测试

var a = {
  name: '张三',
  skills: ['踢球', '跑步', '打羽毛球'],
  age: 18,
  love: {
    name: '小红',
    age: 16
  },
  map: new Map([['aaa', '123']]),
  fn:function(a){
    console.log(`我的名字叫${this.name}` + a)
  },
  set: new Set([1,2,3,4,5])
}
var newA = clone(a)
a.age = 100
a.love.age = 100
a.set.add('1123')
a.skills.push('计算机')
a.name = '小梅'
a.map.set('name', '小明')

console.log(a)
console.log(newA)

a.fn('a')
newA.fn('newA')

闭包

闭包是指有权访问另一个函数作用域中的变量的函数;函数作为返回值,函数作为参数

  • 为什么需要函数嵌套函数?

    • 函数嵌套函数是为了让变量成为局部变量
  • 为什么需要return函数?

    • return函数就是为了让这个函数可以被使用
  • 为什么要让变量成为局部变量呢?

    • 局部变量可以一直被保存在内存中,不会被垃圾回收机制回收。还有就是可以通过闭包来隐藏一个变量,比如在上述例子中我们也完全可以让num成为一个全局变量,但是我又不想让num这个变量的值被任何人任意更改(不想让别人直接访问到这个变量),所以我们可以让它成为局部变量,并且暴露一个函数,让他人可以间接访问。

垃圾回收机制

解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存。

现在各大浏览器通常采用的垃圾回收机制有两种方法:标记清除,引用计数。

内存泄漏

不再用到的内存,没有及时释放,就叫做内存泄漏。

  1. 全局变量引起的内存泄漏
  2. 闭包引起的内存泄漏
  3. 定时器setTimeout setInterval
  4. DOM泄漏
  5. console

1)给DOM对象添加的属性是一个对象的引用 2)元素引用没有清理

💯事件循环 JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

  • macro-task

    • script(整体代码)
    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • UI render
  • micro-task

    • process.nextTick
    • Promise
    • Async/Await(实际就是promise)
    • MutationObserver(html5新特性)

结论

执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

作用域

  1. 全局作用域针对于全局变量
  2. 函数作用域针对于局部变量
  3. 块级作用域

防抖节流

  1. 防抖 触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间; 思路: 每次触发事件时都取消之前的延时调用方法:

应用场景:按钮点击事件/input事件,防止用户多次重复提交

  1. 节流 高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。 思路: 每次触发事件时都判断当前是否有等待执行的延时函数。

应用场景:鼠标/触摸屏的mouseover/touchmove事件 页面窗口的resize事件 滚动条的scroll事件

this指向

  1. 一般函数,this指向全局对象window
  2. 在严格模式下‘use strict’,为undefined
  3. 对象的方法里调用,this指向调用该方法的对象
  4. 构造函数里的this,指向创建出来的实例

改变this指向的方式

  1. .call(thisScope, arg1, arg2, arg3...)
  2. .apply(thiScope, [arg1, arg2, arg3...])
  3. .bind(thisScope, arg1, arg2, arg3...),返回函数

区别

Function.prototype.apply和Function.prototype.call 的作用是一样的,区别在于传入参数的不同; 第一个参数都是,指定函数体内this的指向; 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。 call比apply的性能要好,平常可以多用call, call传入参数的格式正是内部所需要的格式,参考call和apply的性能对比

箭头函数中的 this

由于箭头函数不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值,

所以 call() / apply() / bind() 方法对于箭头函数来说只是传入参数,对它的 this 毫无影响。 考虑到 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。(可以忽略是否在严格模式下的影响)

创建对象

  1. new 操作符 + Object 创建对象
var person = new Object()
person.name = 'lisi'
person.say = function() {
  alert(this.name)
}
  1. 字面式创建对象
var person = {
  name: 'lisi',
  say: function() {
    alert(this.name)
  }
}
  1. 工厂模式
function createPerson(name) {
  var o = new Object()
  o.name = name
  o.sag = function() {
    alert(this.name)
  }

  return o
}
var person1 = createPerson('lisi')
// instanceof无法判断它是谁的实例,只能判断他是对象,构造函数都可以判断出
console.log(person1 instanceof Object); //true
  1. 构造函数模式
function Person(name) {
  this.name = name
  this.say = function() {
    alert(this.name)
  }
}
var person1 = new Person('lisi')
var person2 = new Person('lisi2')
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true
console.log(person1.constructor);      //constructor 属性返回对创建此对象的数组、函数的引用
  • 对比工厂模式有以下不同之处:

    • 没有显式地创建对象
    • 直接将属性和方法赋给了 this 对象
    • 没有 return 语句
  • 以此方法调用构造函数步骤:

    • 创建一个新对象
    • 将构造函数的作用域赋给新对象(将this指向这个新对象)
    • 执行构造函数代码(为这个新对象添加属性)
    • 返回新对象 ( 指针赋给变量person ??? )

可以看出,构造函数知道自己从哪里来(通过 instanceof 可以看出其既是Object的实例,又是Person的实例)

构造函数也有其缺陷,每个实例都包含不同的Function实例( 构造函数内的方法在做同一件事,但是实例化后却产生了不同的对象,方法是函数 ,函数也是对象)

  1. 原型模式
function Person() {}

Person.name = 'lisi'
Person.say = function() {
  alert(this.name)
}
console.log(Person.prototype) // Object{name:'lisi'}
var person1 = new Person()
console.log(person1.name) // lisi
var person2 = new Person()
perosn2.name = 'wangwu'

原型模式的好处是所有对象实例共享它的属性和方法(即所谓的共有属性),此外还可以设置实例自己的属性(方法)(即所谓的私有属性),可以覆盖原型对象上的同名属性(方法)。

  1. 混合模式(构造函数模式+原型模式)
function Person(name) {
  this.name = name
}

Person.Prototype = {
  constructor: Person, // 每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针
  say: function() {
    alert(this.name)
  }
}

可以看出,混合模式共享着对相同方法的引用,又保证了每个实例有自己的私有属性。最大限度的节省了内存。

💯原型与原型链

MDN对原型与原型链的解释

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为null。根据定义,null没有原型,并作为这个原型链中的最后一个环节。

记住

每个实例对象都有一个私有属性:proto ,该私有属性 指向实例构造函数的原型对象。

在JavaScript中,原型链也是如此。原型对象是节点,而私有属性 proto 就是串联连接两个原型对象节点的”线“。

总结

每一个实例对象都有一个私有属性 proto ,该私有属性总是指向实例构造函数的原型对象; 不同的原型对象”节点“通过 proto 指向进行串联连接,从而形成一条原型链。 原型链的终点为 null 对象,即无中生有。

💯原型继承

  1. 原型链继承
function a() {
  this.name = '张三'
}
a.prototype.getName = function() {
  return this.name
}
function b() {
  this.name = '李四'
}
// 继承了a
b.prototype = new a()
b.prototype.getName = function (){
  return this.name
}
var instance = new b()
console.log(instance.getName()) // '李四'

// 改简单点
function a() {
this.name = '张三'
}
a.prototype.getName = function() {
return this.name;
}

var instance = new a()
console.log(instance.getName()) // '张三'
  1. 借用构造函数
function a() {
  this.colors = ["red","blue","green"]
}
function b() {
  a.call(this) // 继承了a
}
var instance1 = new b();
instance1.colors.push("black")
console.log(instance1.colors) // "red","blue","green","black"
var instance2 = new b()
console.log(instance2.colors) // "red","blue","green"

基本思想:在子类型构造函数的内部调用超类构造函数,通过使用call()和apply()方法可以在新创建的对象上执行构造函数。

  1. 组合继承
function a(name) {
  this.name = name
  this.colors = ["red","blue","green"]
}
a.prototype.sayName = function() {
  console.log(this.name)
}
function b(name, age) {
  a.call(this, name) // 继承属性
  this.age = age
}
// 继承方法
b.prototype = new a()
b.prototype.constructor = b // 这个是为了让b的构造函数重新指回这个类本身,否则的话它会变成之前继承的那个类的构造函数,在后面再调用的时候可能会出现意想不到的情况

b.prototype.sayAge = function() {
  console.log(this.age)
}
var instance1 = new b("Hong",18)
instance1.colors.push("black")
console.log(instance1.colors) // "red","blue","green","black"
instance1.sayName() // "Hong"
instance1.sayAge() // 18
var instance2 = new b("su",20)
console.log(instance2.colors) // "red","blue","green"
instance2.sayName() // "su"
instance2.sayAge() // 20

基本思想:将原型链和借用构造函数的技术组合在一块,从而发挥两者之长的一种继承模式。

  1. 寄生组合继承
function beget(obj){   // 生孩子函数 beget:龙beget龙,凤beget凤。
  var F = function(){};
  F.prototype = obj;
  return new F();
}

function Super(){
  // 只在此处声明基本属性和引用属性
  this.val = 1;
  this.arr = [1];
}

//  在此处声明函数
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};

// Super.prototype.fun3...
function Sub(){
  Super.call(this);   // 核心
  // ...
}

var proto = beget(Super.prototype) // 核心
proto.constructor = Sub // 核心
Sub.prototype = proto // 核心
var sub = new Sub()

alert(sub.val);
alert(sub.arr)

这个方式是最佳方式,但是太麻烦,一般只是课本上用,不多解释

  1. 寄生式继承
function beget(obj){   // 生孩子函数 beget:龙beget龙,凤beget凤。
  var F = function(){}
  F.prototype = obj
  return new F()
}
function Super(){
  this.val = 1
  this.arr = [1]
}
function getSubObject(obj){
  // 创建新对象
  var clone = beget(obj) // 核心
  // 增强
  clone.attr1 = 1
  clone.attr2 = 2
  //clone.attr3...
  return clone
}

var sub = getSubObject(new Super());
alert(sub.val) // 1
alert(sub.arr) // 1
alert(sub.attr1) // 1

箭头函数与普通函数

总结

箭头函数写代码拥有更加简洁的语法; 不会绑定this。

  1. 箭头函数不绑定this,使用其所在的上下文this值,作为自己的this
  2. 由于箭头函数不拘于this值,因此箭头函数不能作为构造函数,不能使用new
  3. 箭头函数在调用call、applay等方法时,只需要传入参数,对this值不会做修改
  4. 箭头函数无法通过arguments获取到参数值,需要通过...rest进行获取
  5. 箭头函数没有原型属性
  6. 箭头函数不能当做Generator函数,不能使用yield关键字

💯跨域问题

跨域问题是浏览器同源策略限制,当前域名的js只能读取同域下的窗口属性。

一个网站的网址组成包括协议名,子域名,主域名,端口号。比如https://www.github.com/80

其中https是协议名,www.github.com是子域名,github.com是主域名,端口号是80,当在在页面中从一个url请求数据时,如果这个url的协议名、子域名、主域名、端口号任意一个有一个不同,就会产生跨域问题。

即使是在 http://localhost:80/ 页面请求 http://127.0.0.1:80/ 也会有跨域问题(因为域名不一样嘛~)

PS:浏览器中的 file://域拥有的权限很高,WebKit可以读取磁盘上的文件,IE可以执行CMD,这里大家可以尽情发挥想象,能做的事情太多了!

  • 跨域解决方法小结

    • 最简单也最常见:使用jsonp ,即json with padding(内填充),顾名思义,就是把JSON填充到一个盒子里
    • 一劳永逸:直接在服务器端设置跨域资源访问 CORS(Cross-Origin Resource Sharing),设置Request Header头中Access-Control-Allow-Origin为指定可获取数据的域名
    • 简单有效:直接请求一张图片
    • 找”爸爸”:通过修改document.domain来跨子域
    • 哥俩好:通过window.name来跨域接收数据
    • 新石器时代:使用HTML5的window.postMessage方法跨域
  • jsonp

核心思想:浏览器的script、img、iframe标签是不受同源策略限制的 ,所以通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的callback函数,并把把我们需要的json数据作为参数传入。在服务器端,当req.params参数中带有callback属性时,则把数据作为callback的参数执行,并拼接成一个字符串后返回。

  • 优点:兼容性好,在很古老的浏览器中也可以用,简单易用,支持浏览器与服务器双向通信。

  • 缺点:只支持GET请求,且只支持跨域HTTP请求这种情况(不支持HTTPS)

  • CORS

核心思想:在服务器端通过检查请求头部的origin,从而决定请求应该成功还是失败。具体的方法是在服务端设置Response Header响应头中的Access-Control-Allow-Origin为对应的域名,实现了CORS(跨域资源共享),这里出于在安全性方面的考虑就是尽量不要用 *,但对于一些不重要的数据则随意,例如图片。下图是某公司阿里云服务器上的CORS设置

  • 图片ping

因为在浏览器中,JS脚本和图片是可以跨域的,所以我们可以直接新建一个图片对象,然后在地址中存放一些简单,这种方法只支持get秦秋,且只能单向地向服务器发送请求,在统计广告曝光次数中比较常见。

var img = new Image()
img.onload = function() {
  alert('Done!)
}

img.src = 'http://localhost:3000/name=Yong'
  • 寻找相同主域document.domain

对于以下的这两个域名,可以看到他们的主域名都是 example.com,相同于有一个共同的爸爸,且此方法只适用于两个iframe之间的跨域。

  • window.name

window对象中其实包含了黑魔法的,window.name属性就是其中之一,不同域的框架把想要共享的信息放在window.name里面,且此方法只适用于两个iframe之间的跨域。

  • HTML5 中的window.postMessage方法

window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。这种方法不能和服务端交换数据,只能在两个窗口(iframe)之间交换数据

ajax

  • 优点
    • 交互性更好。来自服务器的新内容可以动态更改,无需重新加载整个页面。
    • 减少与服务器的连接,因为脚本和样式只需要被请求一次。
    • 状态可以维护在一个页面上。JavaScript 变量和 DOM 状态将得到保持,因为主容器页面未被重新加载。
    • 基本上包括大部分 SPA 的优点。

缺点

  • 动态网页很难收藏。
  • 如果 JavaScript 已在浏览器中被禁用,则不起作用。
  • 有些网络爬虫不执行 JavaScript,也不会看到 JavaScript 加载的内容。
  • 基本上包括大部分 SPA 的缺点。
var Ajax = {
  get: function(url,callback){
    // XMLHttpRequest对象用于在后台与服务器交换数据
    var xhr=new XMLHttpRequest();
    xhr.open('GET',url,false);
    xhr.onreadystatechange=function(){
      // readyState == 4说明请求已完成
      if(xhr.readyState==4){
        if(xhr.status==200 || xhr.status==304){
          console.log(xhr.responseText);
          callback(xhr.responseText);
        }
    }
    }
    xhr.send();
  },

  // data应为'a=a1&b=b1'这种字符串格式,在jq里如果data为对象会自动将对象转成这种字符串格式
  post: function(url,data,callback){
    var xhr=new XMLHttpRequest();
    xhr.open('POST',url,false);
    // 添加http头,发送信息至服务器时内容编码类型
    xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    xhr.onreadystatechange=function(){
      if (xhr.readyState==4){
        if (xhr.status==200 || xhr.status==304){
          // console.log(xhr.responseText);
          callback(xhr.responseText);
        }
      }
    }
    xhr.send(data);
  }
}
贡献者: mankueng