面试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这个变量的值被任何人任意更改(不想让别人直接访问到这个变量),所以我们可以让它成为局部变量,并且暴露一个函数,让他人可以间接访问。
垃圾回收机制
解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存。
现在各大浏览器通常采用的垃圾回收机制有两种方法:标记清除,引用计数。
内存泄漏
不再用到的内存,没有及时释放,就叫做内存泄漏。
- 全局变量引起的内存泄漏
- 闭包引起的内存泄漏
- 定时器setTimeout setInterval
- DOM泄漏
- 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新特性)
结论
执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
作用域
- 全局作用域针对于全局变量
- 函数作用域针对于局部变量
- 块级作用域
防抖节流
- 防抖 触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间; 思路: 每次触发事件时都取消之前的延时调用方法:
应用场景:按钮点击事件/input事件,防止用户多次重复提交
- 节流 高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。 思路: 每次触发事件时都判断当前是否有等待执行的延时函数。
应用场景:鼠标/触摸屏的mouseover/touchmove事件 页面窗口的resize事件 滚动条的scroll事件
this指向
- 一般函数,this指向全局对象window
- 在严格模式下‘use strict’,为undefined
- 对象的方法里调用,this指向调用该方法的对象
- 构造函数里的this,指向创建出来的实例
改变this指向的方式
- .call(thisScope, arg1, arg2, arg3...)
- .apply(thiScope, [arg1, arg2, arg3...])
- .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 相关的规则都将被忽略。(可以忽略是否在严格模式下的影响)
创建对象
- new 操作符 + Object 创建对象
var person = new Object()
person.name = 'lisi'
person.say = function() {
alert(this.name)
}
- 字面式创建对象
var person = {
name: 'lisi',
say: function() {
alert(this.name)
}
}
- 工厂模式
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
- 构造函数模式
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实例( 构造函数内的方法在做同一件事,但是实例化后却产生了不同的对象,方法是函数 ,函数也是对象)
- 原型模式
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'
原型模式的好处是所有对象实例共享它的属性和方法(即所谓的共有属性),此外还可以设置实例自己的属性(方法)(即所谓的私有属性),可以覆盖原型对象上的同名属性(方法)。
- 混合模式(构造函数模式+原型模式)
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 对象,即无中生有。
💯原型继承
- 原型链继承
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()) // '张三'
- 借用构造函数
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()方法可以在新创建的对象上执行构造函数。
- 组合继承
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
基本思想:将原型链和借用构造函数的技术组合在一块,从而发挥两者之长的一种继承模式。
- 寄生组合继承
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)
这个方式是最佳方式,但是太麻烦,一般只是课本上用,不多解释
- 寄生式继承
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。
- 箭头函数不绑定this,使用其所在的上下文this值,作为自己的this
- 由于箭头函数不拘于this值,因此箭头函数不能作为构造函数,不能使用new
- 箭头函数在调用call、applay等方法时,只需要传入参数,对this值不会做修改
- 箭头函数无法通过arguments获取到参数值,需要通过...rest进行获取
- 箭头函数没有原型属性
- 箭头函数不能当做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);
}
}