JSONP的原理是什么?
浏览器有同源策略限制,但是script标签的src属性不会被同源策略限制,可以获取任意服务器上的脚本并执行,jsonp通过插入script标签的方式来实现跨域,参数只能通过url传递,只支持GET请求
- 创建callback 方法
- 插入script标签
- 后台接受到请求,解析前端传递过去的callback方法,并返回该方法的调用,把结果当参数传入该方法
- 前端执行服务端返回的方法调用
function jsonp({url, params, callback}) {
return new Promise((resolve, reject) => {
// step1
const script = document.createElement('script')
// step4
window[callback] = function (data) {
resolve(data)
document.body.removeChild(script)
}
// step3
params = {...params, callback}
let paramArray = []
for(let [key, val] of params) {
paramArray.push(`${key}=${val}`)
}
script.src = `${url}${paramArray.join('&')}`
// step2
document.body.appendChild(script)
})
}
function callback(data) {
console.log(data)
}
jsonp({
url: "http://localhost/api/userList",
params: {},
callback: 'callback'
}).then(data => {
console.log(data)
})
// 服务端node 代码
const express = require("express")
const app = express()
app.get("/api/userList", (req, res) => {
const { callback } = req.query
const userList = [
{name: 'garen', age: 20},
{name: 'mike', age: 18},
{name: 'mary', age: 40}
]
res.send(`${callback}(${JSON.stringify(userList)})`)
})
app.listen(3000, () => {
console.log("server is at http://localhost:3000")
})
- 可迭代对象有那些特点?
- 具有Symbol.iterator 属性
- 可以使用for…of 遍历
- 可以被Array.from 转换为数组
- 原生JS具有Iterator的数据结构
- Array,
- Map
- Set
- String
- TypeArray
- 函数的arguments对象
- NodeList对象
let fruits = ['apple', 'banana', 'orange']
let iterator = fruits[Symbol.iterator]()
iterator.next() // {value: 'apple', done: false}
iterator.next() // {value: 'banana', done: false}
iterator.next() // {value: 'orange', done: false}
iterator.next() // {value: undefined, done: true}
- 请实现一个uniq 函数,实现数组去重
uniq([1,2,3,4,5,3,4]) // [1,2,3,4,5]
// 使用Set
function uniq(array) {
return [...new Set(array)]
}
// 使用indexOf
function uniq(array) {
const result = []
for(let i = 0; i < array.length; i++) {
if (result.indexOf(array[i]) === -1) {
result.push(array[i])
}
}
return result
}
// 使用includes
function uniq(array) {
const result = []
for(let i = 0; i < array.length; i++) {
if (!result.indexOf(array[i])) {
result.push(array[i])
}
}
return result
}
// 使用reduce
function uniq(array) {
return arary.reduce((prev, cur) => array.includes(cur) ? prev :[...prev, cur], [])
}
// 使用Map
function uniq(array) {
let map = new Map()
let result = [];
for (let i = 0; i < array.length; i++) {
if (map.has(array[i])) {
map.set(array[i], true)
} else {
map.set(array[i], false)
result.push(array[i])
}
}
return result;
}
- 实现一个flattenDeep函数, 把嵌套数组扁平化
flattenDeep([1, [2, [3, [4]], 5]]); //[1, 2, 3, 4, 5]
//使用 Array.prototype.flat方法
function flattenDeep(arr, deepLength) {
return arr.flat(deepLength)
}
// 利用concat 和 reduce
function flattenDeep(arr) {
return arr.reduce((acc,cur) => {
return Array.isArray(cur) ? acc.concat(flattenDeep(cur)) : acc.concat(cur)
}, [])
}
// 使用staack 无限反嵌套多层数组
function flattenDeep(input) {
const stack = [...input]
const result = []
while(stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next)
} else {
res.push(next);
}
}
return result.reverse();
}
实现Promise.all 方法
- 传入的参数为空的可迭代对象, Promise 会同步返回一个已完成的promise
- 传入的参数不包含任何promise, promise 会异步返回一个已完成的promise
- 其他情况下 Promise.all 返回一个处理中的promise
- 如果传入的参数promise 都变成完成状态,Promise.all返回的promise 都变成已完成
- 如果传入的参数中,又一个promise失败,Promise.all将失败的结果返回给失败状态的promise, 而不管promise 是否完成
任何情况下,promise.all 返回的promise 的完成状态结果都是一个数组
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
// 转化为数组
promises = Array.from(promises)
if (promises.length === 0) {
resolve([])
} else {
let result = []
let index = 0;
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(data => {
result[i] = data[i]
index++
// 记录成功的数量和promises的长度相等,状态变为fulfilled
if(index === promises.length) {
resolve(result)
}
}, err => {
reject(err)
return;
})
}
}
})
}
什么是闭包? 闭包的作用是什么?
- 闭包是什么
- 闭包是指有权访问另一个函数作用域中变量的函数 - javascript 高级程序设计
- 从技术角度讲,所有的javascript函数都是闭包:它们都是对象,它们都关联到作用域 - javascript权威指南
- 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数在当前词法作用域之外执行 - 你不知道的javascript
- 闭包是什么
functioon foo() {
var a = 2;
// foo执行后,foo 内部的作用域不会销毁,因为闭包让fn可以加继续访问定义时的词法作用域
return function fn() {
console.log(a + 1)
}
}
let func = foo();
func(); // 3
- 闭包的作用
- 能够反问函数定义是所在的词法作用域,阻止其被回收
- 私有化变量
- 模拟块级作用域
- 创建模块
function base() {
let x = 10; // 私有变量
return {
getX: function() {
return x
}
}
}
base().getX() // 10;
// 模拟块级作用域
var a = []
for(var i = 0; i < 10; i++) {
a[i] = (function(j){
return function() {
console.log(j)
}
})(i)
}
a[6]() // 6;
// 创建模块
function personModule() {
let name = "garen"
let age = 20
function sayName() {
console.log(name)
}
function sayAge() {
console.log(age)
}
return {
sayName,
sayAge,
}
}
personModule().sayName();
- 节流函数的作用是什么? 有哪些应用场景,请实现一个节流函数
- 节流函数的作用是规定一个单位时间内,并在这个单位时间内最多只能触发一次函数,多次触发,只有一次生效
- 节流函数的应用场景
- 按钮点击事件
- 拖拽事件
- onScroll
- 计算鼠标移动的距离
// 禁用第一次首先执行,传递 {leading:false}
// 想禁用最后一次执行,传递 {trailing:false}
// 看不懂?
function throttle(func, wait, options = {}) {
var timeout, context, args, result;
var previous = 0;
var later = function () {
previous = options.leading === false ? 0 : (Date.now() || new Date().getTime())
timeout = null
result = func.apply(context, args)
if(!timeout) context = args = null
}
var throttled = function () {
var now = Date.now() || new Date().getTime()
if (!previous && options.leading === false) previous = now
var remaining = wait - (now - previous)
context = this;
args = arguments
if (remaining <= 0 || remaining > wait) {
if(timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now;
result = func.apply(context, args)
if(!timeout) context = args = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
return result;
}
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
}
return throttled
}
- 防抖函数的作用是什么? 请实现一个防抖函数
- 防抖函数的作用就是控制函数在一定时间内执行的次数,防抖意味这N秒内函数只会被执行一次, 如果N秒内再次被触发,就重新计算延迟时间
- 防抖的应用场景
- 搜索框输入查询
- 表单验证
- 按钮提交事件
- 浏览器窗口缩放,resize 事件
function debounce(func, wait, immediate = true) {
let timeout, result;
let later = (context, args) => {
return setTimeout(() => {
if (!immediate) {
result = func.apply(context, args)
context = args = null
}
}, wait)
}
let debouned = function(...params) {
if(!timeout) {
timeout = later(this, params)
if (immediate) {
result = func.apply(this, params)
}
} else {
clearTimeout(timeout)
timeout = later(this, params)
}
}
debouned.cancel = function() {
clearTimeout(timeout)
timeout = null;
}
return debouned
}
思考: 防抖和节流的区别?
**
说一说你对JS执行上下文栈和作用域链的理解?
- JS执行上下文, 指的是Js 代码被解析和执行时所在环境的抽象概念
- 全局上下文
- 函数执行上下文
- 执行上下文的创建过程
- 创建变量对象,初始化函数的参数arguments, 提升函数声明和变量声明
- 创建作用域链,在执行期上下文的创建阶段,作用域链是在变量对象之后创建的
- 确定this的值,即ResolveThisBinding
- 作用域链, 负责收集收集和维护所有声明的标识符组成的一系列查询,并实施一套非常严格的规则,确定当前当前执行代码对这些标识符的访问权限
- 作用域工作模型
- 词法作用域(JS 使用的),意味作用域是书写代码时的变量和函数声明决定的
- 动态作用域
- 作用域分为
- 全局作用域
- 函数作用域
- 块级作用域
- 作用域工作模型
- JS执行上下文
- 执行栈,具有LIFO结构,用于存储代码执行期间创建的所有执行上下文
- 首次运行JS代码,会创建一个全局执行上下文并push到当前栈中,每当发生函数执行调用,引擎都会为该函数创建一个新的函数执行上下文并push到当前执行栈顶
- 当栈顶的函数运行完成后,其对应的函数执行上下文从执行栈pop出,上下文的控制权将移动执行栈的下一个上下文
- 执行栈,具有LIFO结构,用于存储代码执行期间创建的所有执行上下文
- 作用域链,就是从当前作用域链开始一层一层向上寻找某个变量,直到找到全局作用域还没有找到,就放弃,这种一层一层的关系,就是作用域链
- JS执行上下文, 指的是Js 代码被解析和执行时所在环境的抽象概念
let, const, var 的区别有哪些
- let/const 无变量提升,而var有变量提升
- 相同作用域中,let/const不允许重复声明,var允许重复声明
- const 声明变量是必须设置初始值
- let/const 存在暂时性死区,var不存在
- let/const 存在块级作用域,var不存在
- const 声明是一个只读的常量,不可以改变,指的是存储在栈中的地址不变,但是地址对应的存储在堆中的值是可以改变的
隐藏页面中某个元素的方法有哪些?
- 完全隐藏
- display: none
- , H5新增
- 视觉上的隐藏
- 设置position或者盒模型,将元素移出可视范围内
- 利用transform 移动出范围外,缩放为0,rotate
- 设置其大小为0
- 设置透明度为0
- 设置visibility: hidden
- 层级覆盖,z-index属性
- clip-path 剪切,clip-path: polygon(0 0, 0 0, 0 0, 0 0)
- 语义上的隐藏
- , 读屏幕软件不可读,占据空间,可见
- 完全隐藏
es5有几种继承方式,分别有哪些优缺点
- 原型链继承
// 原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法
// 缺点:
// 原型引用的类型属性会被所有实例共享
// 创建子类型的实例时,无法在不影响所有对象实例情况下给超类型的构造函数中传递参数
function SuperType(){
this.name = "Sub"
this.colors = ['pink','blue', 'green']
}
SuperType.prototype.getName = function() {
return this.name;
}
function SubType() {
this.age = 22;
}
SubType.prototype = new SuperType()
SubType.prototype.getAge = function() {
return this.gae
}
SubType.prototype.constructor = SubType
let instance1 = new SubType()
instance1.colors.push('yellow')
instance1.getName(); // "Sub"
instance1.colors: ['pink','blue', 'green', 'yellow']
let instance2 = new SubType()
instance2.colors: ['pink','blue', 'green', 'yellow']
- 借用构造函数
// 在子类型的构造函数中调用超类型构造函数。
// 优点
// 可以向超类型传递参数
// 解决了原型中引用类型值被所有实例共享的问题
// 缺点
// 方法在构造函数中定义,函数无法复用
// 超类型中定义的方法对子类不可见
function SuperType(){
this.name = "Sub"
this.colors = ['pink','blue', 'green']
}
function SubType() {
SuperType.call(this, name)
}
let instance1 = new SubType()
instance1.colors.push('yellow')
instance1.colors: ['pink','blue', 'green', 'yellow']
let instance2 = new SubType()
instance2.colors: ['pink','blue', 'green']
- 组合式继承
// 组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。
// 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性
// 缺点
// 会调用两次超类型,一次在创建子类型原型是,另一次在子类型的构造函数内
// 优点
// 可以向超类传递参数
// 每个实例都有自己的属性
// 实现了函数复用
function SuperType(name){
this.name = name
this.colors = ['pink','blue', 'green']
}
SuperType.prototype.getName = function() {
return this.name;
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.getAge = function() {
return this.age;
}
let instance1 = new SubType('Sub', 20)
instance1.colors.push('yellow')
instance1.getName(); // "Sub"
instance1.colors: ['pink','blue', 'green', 'yellow']
let instance2 = new SubType('Jack', 18)
instance2.colors: ['pink','blue', 'green']
instance2.getName() // Jack
- 原型式继承
// 借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
// 缺点
// 包含引用的值的属性会被所有实例共享
function object(o) {
function F() {}
F.prototype = o
return new F()
} // 等同于 ES6 的 Object.create
var person = {
name: 'garen',
colors: ['pink','blue', 'green']
}
var person1 = Object.create(person)
person1.name = 'jack'
person1.colors.push('red')
var person2 = Object.create(person)
person2.name = 'mike'
person2.colors.push('yellow')
person1.colors // ["pink", "blue", "green", "red", "yellow"]
person2.colors // ["pink", "blue", "green", "red", "yellow"]
- 寄生式继承
// 创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象
// 缺点
// 为对象添加函数,不能做到函数复用而效率低下
// 包含引用的类型的值会被所有实例共享
function createAnother(o) {
var clone = Object.create(o)
clone.say = function() {
console.log('hi')
}
return clone
}
var person = {
name: 'garen',
colors: ['pink','blue', 'green']
}
var person2 = createAnother(person)
person2.say(); // hi
- 寄生组合式继承
// 即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
// 寄生组合式继承就是一个借用构造函数 + 相当于浅拷贝父类的原型对象
// 优点:
// 只调用了一次超类型构造函数,效率更高,
// 避免在 SubType.prototype上面创建不必要的、多余的属性,与其同时,原型链还能保持不变
function inherit(subType, superType) {
// 为超类型创建一个副本
var prototype = Object.create(superType.prototype)
// 为创建的副本添加constructor属性
prototype.constructor = subType
// 为新创建的对象赋值给子类型的属性
subType.prototype = prototype
}
function SuperType(name){
this.name = name
this.colors = ['pink','blue', 'green']
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
inherit(SubType, SuperType);
- 异步加载JS脚本的方式有哪些
- script 标签中增加async 或者defer 属性,就会异步加载脚本
- defer 要等到整个页面渲染结束,即DOM结构生成,其他脚本执行完毕,在window.onload之前执行
- async一旦下载完毕,引擎就会中断渲染并执行完此脚本,再继续渲染
- 多个defer脚本,会按照他们在页面出现的顺序加载
- 多个async脚本,不能保证加载顺序
- 动态创建script标签 ```javascript let script = document.createElement(“script”) script.src = ‘./jquery.js’ document.body.append(script)
- script 标签中增加async 或者defer 属性,就会异步加载脚本
- XHR异步加载JS
```javascript
let xhr = new XMLHttpRequest();
xhr.open('get', 'js/jquery.js', true);
xhr.send()
xhr.onreadystatechange = function() {
if(xhr.readystate === 4 && xhr.status === 200) {
eval(xhr.responseText);
}
}
- 什么是 BFC? BFC 的布局规则是什么? 如何创建BFC?
- BFC 是块级格式上下文
- BFC 布局规则
- BFC内,盒子按顺序垂直排列
- BFC内, 两个盒子的垂直距离由margin属性决定,同一个BFC相邻的盒子的margin会用较大的合并
- BFC内, 每个盒子的左外边缘接触内部盒子的左边缘,即使在浮动下也是如此
- BFC区域内不会和 float box 重叠
- BFC 是页面上一个隔离的独立容器,容器内子元素不会影响到容器外的元素
- 计算BFC的高度,浮动元素也参与计算
- 创建BFC
- 跟元素
- 浮动元素,float 不为none
- position absolute 或者fixed
- overflow 不为 visible
- display 为 inline-block, table-cell, table-caption
- BFC 的应用
- 防止margin重叠
- 清除内部浮动
- 自适应多栏布局 (BFC的区域不会与float box重叠。因此,可以触发生成一个新的BFC)
// 6. 如何让 (a == 1 && a == 2 && a == 3) 的值为true?
// 1. 隐式类型转换
// 如果部署了 [Symbol.toPrimitive] 接口,那么调用此接口,若返回的不是基本数据类型,抛出错误。
// 如果没有部署 [Symbol.toPrimitive] 接口,那么根据要转换的类型,先调用 valueOf / toString
// 非Date类型对象, hint 是 default 时,调用顺序为: valueOf >>> toString,即 valueOf 返回的不是基本数据类型,才会继续调用 valueOf,如果 toString 返回的还不是基本数据类型,那么抛出错误。
// 如果 hint 是 string(Date对象的hint默认是string) ,调用顺序为: toString >>> valueOf,即 toString 返回的不是基本数据类型,才会继续调用 valueOf,如果 valueOf 返回的还不是基本数据类型,那么抛出错误。
// 如果 hint 是 number,调用顺序为: valueOf >>> toString
let a = {
[Symbol.toPrimitive]: function(hint) {
let i = 1;
return function() {
return i++
}
}
}
// 2. 利用数据劫持
let i = 1;
let a = new Proxy({}, {
i: 1,
get: function() {
return () => this.i++;
}
})
// 3. 数组的toString 接口默认调用数组的join方法,重新join方法
let a = [1,2,3]
a.join = a.shift
- 柯理化函数实现
// 函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
// 柯里化的作用
// 参数复用
// 提前返回
// 延迟执行
const curry = (fn, ...args) => args.length < fn.length
? (...arguments) => curry(fn, ...args, ...arguments): fn(...args)
function sum(a, b, c) {
return a + b + c
}
var sum = curry(sum)
sum(2)(3)(5)
sum(2,3 ,5)
sum(2)(3,5)
sum(2,3,5)
- call / apply 的实现原理是什么?
- call 方法中获取调用的call函数
- 如果第一个参数没有传入,默认指向window / global
- 传入的call的第一个参数是this指向的函数,根据隐式绑定的规则
- 返回执行结果
Function.prototype.call = function() {
let [thisArg, ...args] = [...arguments]
if (!thisArg) {
thisArg = typeof window === 'undefined' ? global : window
}
thisArg.func = this;
let result = thisArg.func(...args)
delete thisArg.func
return result;
}
Function.prototype.apply = function() {
let [thisArg, ...args] = [...arguments]
if (!thisArg) {
thisArg = typeof window === 'undefined' ? global : window
}
thisArg.func = this;
let result;
if(!args.length) {
result = thisArg.func()
} else {
result = thisArg.func(...args)
}
delete thisArg.func
return result;
}
- 浅拷贝和深拷贝的区别是什么? 实现一个深拷贝
- 深拷贝,对象与原来对象完全隔离,互不影响,层层拷贝
- 浅拷贝,对象的每个属性进行拷贝,对象属性是引用类型时,复制引用地址,当引用的值发生改变也会发生变化,只拷贝一层, 使用 for in, Object.assign, 扩展运算符…, Array.prototype.slice,Array.prototype.concat
- 深拷贝实现
- JSON.parse(JSON.stringify(obj))
- 对象属性是函数,无法拷贝
- 原型链上属性无法拷贝
- 不能正确处理Date类型数据
- 不能处理RegExp
- 会忽略symbol
- 会忽略undefined
- 实现一个deepClone
- 基本数据类型,直接返回
- 如果是RegExp或者Date类型,返回对应类型
- 如果是复杂类数据类型,递归
- 考虑循环引用的问题
- JSON.parse(JSON.stringify(obj))
function deepClone(obj, hash = new WeakMap()) {
if (obj instanceof RegExp) return new RegExp(obj)
if (obj instanceof Date) return new Date(obj)
if (obj === null || typeof obj !== 'objetc') retrun obj;
if (hash.has(obj)) return hash.get(obj)
let t = new obj.constructor();
hash.set(obj, t)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
t[key] = deepClone(obj[key], hash)
}
}
return t;
}
- 如何判断this 的指向?
- 谁调用它, this 就指向谁
- 全局环境中的this, 浏览器指向window, node指向 global
- 是否绑定new
- new 绑定,并且构造函数没有返回function/ object, this指向这个新对象
- new绑定, 并且构造函数返回的是function/object,返回的构造函数中创建的对象,this 指向构造函数中创建的对象
- 是否经过call,apply 调用,或者使用bind, 即显式绑定
- 如果是,this指向绑定的对象
- 如果第一个参数传入的是null/undefined, 严格模式下this为null/undefined, 非严格模式下,this指向全局对象
- 隐式绑定,函数的调用是在某个对象上触发,即调用的位置上存在上下文对象,典型的 xxx.fn()
- 箭头函数, 没有自己的this, 继承外层上下文绑定的this
,
- new的实现原理是什么?
- 创建一个空对象,构造函数中的this指向这个空对象
- 控对象被执行原型链接
- 执行构造函数方法,属性和方法被添加到this的引用对象中
- 如果构造函数没有返回其他对象,即返回this, 否则返回构造函数中创建的对象
function _new() {
let target = {} //创建一个空对象
let [constructor, ...args] = [...arguments]
target.__proto__ = constructor.prototype // 执行原型链接,target是constructor的实例
// 执行构造函数方法,属性和方法田间到创建的对象上
let result = constructor.apply(target, args);
// 如果构造函数执行结构返回的是一个对象,那么就返回这个对象
if (result && (typeof result === 'object' || typeof result === 'function')) {
return result;
}
// 如果构造函数返回的不是一个对象,返回创建的新对象
return target
}