前端题目

下文章来源于前端宇宙 ,作者刘小夕,本文只记录原文,并加上个人记录,仅供自学。

 

1、new 的实现原理是什么?

new 的实现原理:

  1. 创建一个空对象,构造函数中的 this 指向这个空对象;

  2. 这个新对象被执行 [[原型]] 连接;

  3. 执行构造函数方法,将属性和方法添加到 this 应用的对象中;

  4. 如果构造函数中没有返回其他对象,那么返回 this ,即创建的这个新对象,否则,返回构造函数中返回的对象。

function _new() {
    let target = {}; // 创建的新对象
    // 第一个参数是构造函数
    let [constructor, ...args] = [...arguments];
    // 执行 [[原型]]链接; target 是 constructor 的实例
    target.__proto__ = constructor.prototype;
    // 执行构造函数,将属性和方法添加到创建的空对象上
    let result = constructor.apply(target, args);
    if(result && (typeof (result) == 'object' || typeof (result) == 'function')) {
        // 如果构造函数执行的构造返回的是一个对象(或函数),那么返回这个对象
        return result;
    }
    // 如果构造函数返回的不是一个对象,返回创建的新对象
    return target;
}

 

2、如何正确判断 this 的指向?

如果用一句话说明 this 的指向,那么即是:谁调用它,this 就指向谁。

 

但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:

this 的指向可以安装以下顺序判断:

  全局环境中的 this:

  浏览器环境:无论是在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window;

  node 环境:无论是在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向 空对象 {};

 

  是否是 new 绑定

  如果是 new 绑定,并且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。

  如下:

构造函数返回值不是 function 或 object。newSuper() 返回的是 this 对象。
function Super(age) {
    this.age = age
}

let instance = new Super('26')
console.log(instance.age) // 26
构造函数返回值是 function 或 object,newSuper() 返回的是 Super 中返回的对象。
function Super(age) {
    this.age = age
    let obj = {a: '2'}
    return obj;
}

let instance = new Super('hello');
console.log(instance) // {a: '2'}
console.log(instance.age) // undefined

 

函数是否通过 call, apply 调用,或者使用了 bind 绑定,如果是,那么 this 绑定的就是指定的对象 【归结为显示绑定】

function info() {
    console.log(this.age)
}
var person = {
    age: 20,
    info
}

var age = 28;
var info = person.info
info.call(person)     // 20        
info.apply(person)  // 20
info.bind(person)   // 20

这里同样需要注意一种特殊情况,如果 call,apply 或者 bind 传入的第一个参数值是 undefined 或 null,严格模式下 this 的值为传入的值 undefined / null。非严格模式下,实际应用的默认绑定规则,this 指向全局对象(node 环境为 global, 浏览器环境为 window)

function info(){
    // node环境中:非严格模式 global, 严格模式为 null
    // 浏览器环境中:非严格模式 window,严格模式为 null
    console.log(this)
    console.log(this.age)
}

var person = {
    age: 20,
    info
}

var age = 28;
var info = person.info; // info 指向的是 顶部定义的 info 函数

info.call(null)
// 严格模式抛出错误;
// 非严格模式,node 下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式,浏览器环境下输出 28(因为全局的 age 会挂在 window 上)

 

隐式绑定,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的隐式调用为:xxx.fn()

function info(){
    console.log(this.age)
}
var person = {
    age: 20,
    info
}
var age = 28;
person.info() // 20; 执行的是隐式绑定

 

默认绑定,在不能应用其他绑定规则时使用的默认规则,通常是独立函数调用。

非严格模式:node环境,执行全局对象 global,浏览器环境,执行全局对象 window。

严格模式:执行 undefined

function info() {
    console.log(this.age)
}
var age = 28;

info()
// 严格模式: 抛错 ,因为 this 此时是 undefined
// 非严格模式:node 环境下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式:浏览器环境下输出 28(因为全局的 age 会挂在 window 上)

 

箭头函数的情况:

箭头函数没有自己的 this,继承外层上下文绑定的 this。

let obj = {
    age: 20,
    info: function () {
        return () => {
            console.log(this.age); // this 继承的是外层上下文绑定的 this
        }
    }
}

let person = {age: 28}
let info = obj.info();
info() // 20

let info2 = obj.info.call(person)
info2() // 28

 

3、call / apply / bind 的区别

call / apply / bind 的作用是改变函数运行时 this 的指向。

 

call / apply 后面传的参数形式不同。

call 需要把 实参 按照 形参 的个数传进去,而 apply 需要传一个数组(arguments实参列表)

 

bind 和 call  很相似,第一个参数是 this 的指向。第二个参数开始是接收的参数列表。区别在于bind 方法返回值是函数以及 bind 接收的参数列表的使用。

bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而 原函数 中的 this 并没有被改变。

 

4、call / apply 的实现原理是什么?

call 和 apply 的功能相同,都是改变 this 的指向,并立即执行函数。区别在于传参方式不同。

  • func.call(thisArg, arg1, arg2, ...):第一个参数是 this 指向的对象,其他参数依次传入。
  • func.apply(thisArg, [argsArray]):第一个参数是 this 指向的对象,第二个参数是数组或类数组。

 

一起思考下,如何模拟 call ?

首先,我们知道,函数都可以调用 call,说明 call 是函数原型上的方法,所有的实例都可以调用。即 Function.prototype.call。

  • 在 call 方法中获取调用 call() 函数
  • 如果第一个参数没有传入,那么默认指向 window/global(非严格模式)
  • 传入 call 的第一个参数是 this 指向的对象,根据隐式绑定的规则,我们知道 obj.foo(),foo() 中的 this 指向 obj;因此我们可以这样调用函数  thisArg.func(...args)
  • 返回执行结果
Function.prototype.call = function () {
    let [thisArg, ...args] = [...arguments]

    if(!thisArg){
        // context 为 null 或者是 undefined
        thisArg = typeof window === 'undefined' ? global : window
    }
    // this 指向的是 当前函数 func (func.call)
    thisArg.func = this

    // 执行函数
    let result = thisArg.func(...args)
    delete thisArg.func; // thisArg 上并没有 func  属性,因此需要移除
    
    return result
}

 

apply 的实现思路与 call 一致,仅参数处理略有差别。如下:

Function.prototype.apply = function (thisArg, rest) {
    let result // 函数返回结果
    
    if(!thisArg){
        // context 为 null 或者是 undefined
        thisArg = typeof window === 'undefined' ? global : window
    }

    thisArg.func = this // this 指向的是当前函数 func(func.apply)

    if(!rest){ // 第二个参数 为 null / undefined
        result = thisArg.func()
    } else {
        result = thisArg.func(...rest)
    }

    delete thisArg.func(); // thisArg 上并没有 func 属性,因此需要移除

    return result
}

 

5、深拷贝和浅拷贝的区别是什么?实现一个深拷贝

深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。

 

深拷贝

深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。
深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。

 

浅拷贝

浅拷贝是会将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,
实质复制的是引用地址,当引用执行的值改变时也会跟着变化。

 

可以使用 forin、Object.assign、扩展运算符 ...   Array.prototype.slice()、Array.prototype.concat() 等,例如:

let obj = {
    name: 'yue',
    age: 18,
    hobbies: ['reading', 'photography']
}

let obj2 = Object.assign({}, obj);
let obj3 = {...obj}

obj.name = "Jack"
obj.hobbies.push('coding')

console.log(obj)
// {name: 'Jack', age: 18, hobbies:['reading', 'photography', 'coding']}
console.log(obj2)
// {name: 'yue', age: 18, hobbies: ['reading', 'photography', 'coding']}
console.log(obj3)
// {name: 'yue', age: 18, hobbies: ['reading', 'photography', 'coding']}

  可以看出浅拷贝只对第一层属性进行了拷贝,当第一层的属性值是基本数据类型时,新的对象和原对象互不影响,但是如果第一层的属性值是复杂数据类型,那么新对象和原对象的属性值其指向的是同一块内存地址。

 

深拷贝实现

1. 深拷贝最简单的实现是 JSON.parse(JSON.stringify(obj))

JSON.parse(JSON.stringify(obj)) 是最简单的实现方式,但是有一些缺陷:

1. 对象的属性值是函数时,无法拷贝

2. 原型链上的属性无法拷贝

3. 不能正确的处理 Date 类型的数据

4. 不能处理 RegExp 

5. 会忽略 symbol

6. 会忽略 undefined

 

2. 实现一个 deepClone 函数

1. 如果是基本数据类型,直接返回

2. 如果是 RegExp 或者 Date 类型,返回对应类型

3. 如果是复杂数据类型,递归

4. 考虑循环引用的问题

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 !== 'object'){
        // 如果不是复杂数据类型,直接返回
        return obj
    }

    if(hash.has(obj)){
        return hash.get(obj)
    }

    /**
    * 如果 obj 是数组,那么 obj.constructor 是 [function: Array]
    * 如果 obj 是对象,那么 obj.constructor 是 [function: Object]
    */
    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
}
// 解说: // 1. hash.has(obj) 解决循环引用的问题,地址一样就不用递归了 // 循环引用:对象里又引用了本对象,例如: // let obj = {a: 1} // obj.b = obj // 2. obj.constructor() 中的 constructor 指向的是 创建这个 obj对象的 引用,obj.constructor()是一个构造函数,所以需要new 一下,用 new obj.constructor() 创建了一个 和 obj 一样的 新对象 // 3. for in obj,为什么又要用 hasOwnProperty 来检测这个 key 是否是 obj 自身的属性? // 因为 for in 可能会循环到 proto 上面的,所以用 hasOwnProperty 只检测到 obj 自身的。

 

6、WeakMap /  Map / WeapSet / Set / Object  的区别

Object 作为哈希表存在一下问题

1.  Object 的 key 必须是 String 或者是 Symbol,当 key 不为字符串时,会调用 toString() 进行强制转换,将转换后的字符串作为 key

2.  Object 含有内置属性,如 constructor、toString、valueOf,与其同名的键值会产生冲突,可以使用 Object.cerate(null) 创建一个空对象继承自 null 来避免此问题

3.  Object 其属性可能是不可遍历的、有些属性可能是在原型链上,所以 Object 长度的获取比较繁琐

4.  Object 是不可迭代的,即不能使用 for...of 来遍历,typeof obj[Symbol.iterator] === undefined

5.  Object 是无序的,其元素顺序与添加的顺序无关

 

Object 的使用
// 1.创建
var obj = new Object();            //创建一个空对象
var obj = new Object;              //创建一个空对象
var obj = Object.create(null);    //obj继承null


// 2. 添加
obj['age'] = 11;
obj.age = 11; 


// 3. 获取
obj.age;   //11
obj['age'];  //11


// 4. 判断某个 key 是否存在相应的 value
if(obj.age !== undefined) {}        // 判断是否存在
if('age' in obj) {}        // 判断是否存在


// 5. 删除
delete obj.age    //彻底删除该属性
obj.age = undefined    // 仅仅是改变了值为 undefined,该对象仍然保留有该属性


// 6. 获取大小
Object.keys(obj).length; //Object.keys 只返回对象自身的 可遍历 属性的全部属性名,不包括原型链上的属性


// 7. 遍历
// obj {age: 11, name: 'jack'}

for(var key in obj){
    console.log(`key: ${key}, value: ${obj[key]}`);
    // key: age, value: 11
    // key: name, value: jack
}
Object.keys(obj).forEach((key) => console.log(`key:${key}, value: ${obj[key]}`))

 

Map 更适合用来做哈希表

1.  各种类型的值(包括 object)都可以作为 key

2.  Map 支持迭代,直接使用 for...of 来遍历,而不需要像对象一样先获取 key 再遍历,typeof obj[Symbol.iterator] === function

3.  Map 在遇到频繁删除添加和键值对的场景下,有更好的性能表现

4.  Map 用迭代的方式遍历 key 时,得到的 key 的顺序与 key 添加到 Map 时的顺序相同

 

Map 的使用方式
// 1. 创建
var map = new Map();     // 空 Map
var map = new Map([[1,2],[2,3]]);  // map = {1=>2, 2=>3}


// 2. 添加
map.set(4, 5)     // map = {4=>5},若添加的 key 已存在相应的 value,则覆盖旧 value


// 3. 获取
map.get(4);    // 5,若相应的 value 不存在则返回 undefined


// 4. 判断某个 key 是否存在相应的 value
map.has(4);    // true    通过 key 判断,返回一个 boolean 值


// 5. 删除
map.delete(4);    //删除成功返回 true,返回 false 表示该属性不存在
map.clear()        // 删除所有元素


// 6. 获取大小
map.size()


// 7. 遍历
// map:  { 2=>3, 4=>5}
for(const item of map) {
    console.log(item);
    // Array[2,3]
    // Array[4,5]
}

// 或者
for(const [key, value] of map){
    console.log(`key:${key}, value:${value}`);
    // key: 2, value: 3
    // key: 4, value: 5
}

// 或者
map.forEach((value, key) => console.log(`key: ${key}, value: ${value}`))
// key: 2,   value: 3

 

Set

 类似于数组,没有重复的元素,可迭代。

 

代码示例:

// 创建
var set = new Set()
var set = new Set([1,2,3,4]) // 出了可接收数组作为参数,也可接受具有 iterable 接口的其他数据结构

// 添加
set.add(5)

// 删除
set.delete(5) // 删除某个值,返回一个布尔值,表示删除是否成功
set.clear()    // 清空集合

// 判断成员是否存在
set.has(5) // 返回布尔值,表示该值是否为成员

// 获取大小
set.size;

// 遍历
for(var item of set) {
    console.log(item) // 1  2  3  4
}

 

WeakMap 和 WeakSet

 1.  WeakMap

WeakMap 跟 Map 结构类似,也拥有 get、has、delete 等方法,使用用法和使用用途都一样。

 

不同之处:

1.  WeakMap 只接受对象作为键名,但 null 不能作为键名

2.  WeakMap 不支持 clear 方法,不支持遍历,也就没有了 keys、value、entries、 forEach 这 4 个方法,也没有属性 size

3.  WeakMap 键名中的引用类型是弱引用,假如这个引用类型的值被垃圾机制回收了,WeakMap 实例中的对应键值对也会消失。WeakMap 中的 key 不计入垃圾回收,即若只有 WeakMap 中的 key  对某个对象有引用,那么此时执行垃圾回收时就会回收该对象,而 Map 中的 key 是计入垃圾回收

假如这个引用类型的值被垃圾机制回收了,WeakMap 实例中的 对应键值对也会消失。
WeakMap 中的 key 不计入垃圾回收,而 Map 中的 key 是计入垃圾回收。

解说:
  对于对象而言,每有一个引用,引用计数加 1,等引用计数为 0 的时候就可以垃圾回收了。
  如果只有 Map 引用一个对象,那么这个对象不能回收。
  如果只有 WeakMap 引用一个对象,那么这个对象可以被回收。
  这个回收指的是 引用的对象,而值 key,这个是说 WeakMap 中的 key 不会引起引用计数 +1

MDN 的说法:
  在 JavaScript 里,map API 可以通过使其四个 API 方法共用两个数组(一个存放键,一个存放值)来实现。给这种 map 设置值时会同时将键和值添加到这两个数组的末尾。从而使得键和值的索引在两个数组中相对应。当从该 map 取值的时候,需要遍历所有的键,然后使用索引从存储值的数组中检索出相应的值。

  但这样的实现会有两个很大的缺点,首先赋值和搜索操作都是 O(n) 的时间复杂度( n 是键值对的个数),因为这两个操作都需要遍历全部整个数组来进行匹配。另外一个缺点是可能会导致内存泄漏,因为数组会一直引用着每个键和值。这种引用使得垃圾回收算法不能回收处理它们,即使没有其他任何引用存在了。

  相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。

  正由于这样的弱引用,WeakMap 的 key 是不可枚举的(没有方法能给出所有的 key )。如果 key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果。因此,如果你想要这种类型对象的 key 值的列表,你应该使用 Map。

  基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。

 

2.  WeakSet

WeakSet 结构与 Set 类似,但只有有 add、delete、has 三个方法

 

不同之处:

1.  WeakSet 的成员只能是对象,并且 WeakSet 不支持 clear 方法,不支持遍历,也没有 forEach 这个方法,没有属性 size

2.  WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,如果只有 WeakSet 引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存。

 

7、说说 Object.assign 和扩展运算符 ... 

 Object.assign  : 浅拷贝

扩展运算符:浅拷贝

 

8、柯里化函数实现

在开始之前,我们首先需要搞清楚函数柯里化的概念。

 

函数柯里化是把接受多个参数的函数变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

 

举例:add 函数

// 普通 add 函数
function add(x, y){
  return x + y  
}

// 柯里化后
function curryingAdd(x) {
    return function(y) {
        return x + y
    }
}

add(1, 2) // 3
curryingAdd(1)(2) // 3

实际上就是把 add 函数的 x,y 两个参数变成了先用一个函数接收 x,然后返回一个函数去处理 y 参数。

现在思路应该比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

 

柯里化的实现:

const curry = (fn, ...args) =>
    args.length < fn.length
        // 参数长度不足时,重新柯里化该函数,等待接受新参数
     // fn -->  sumFn(a,b,c){return a+b+c}  fn.length 指函数的形参个数
        ? (...arguments) =>curry(fn, ...args, ...arguments)
        // 参数长度满足时,执行函数
        : fn(...args)


// 换种写法
const curry = (fn, ...args) => {
        // args.length < fn.length ? (...arguments) =>curry(fn, ...args, ...arguments) : fn(...args)

        // fn --> sumFn(a, b, c){return a+b+c}
     console.log('args-->', args)
     // []
     // [2]
     // [2, 3]
     // [2, 3, 5]
  if(args.length < fn.length){ // 参数长度不足时,重新柯里化该函数,等待接受新参数 
    return (...arguments) =>{
      // console.log(
'arguments-->', arguments)
      // [2]
      // [3]
      // [5]
      return curry(fn, ...args, ...arguments)
    }
  }
else { // 参数长度满足时,执行函数
    return fn(...args)
  }
}
function sumFn(a, b, c){
    return a+b+c
}

var sum = curry(sumFn)

console.log(sum(2)(3)(5)) // 10    sum(2)(3)(5)  --> curry(sumFn)(2)(3)(5)
console.log(sum(2, 3, 5))  // 10
console.log(sum(2)(3, 5)) // 10
console.log(sum(2, 3)(5)) // 10

 

函数柯里化的主要作用:
  •  参数复用
// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')     // false
check(/[a-z]+/g, 'test') // true

// Currying 后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')     //true
hasNumber('testtest') // false
hasLetter('21212')     // false

上面的示例是一个正则的校验,正常来说直接调用 check 函数就可以了,但是如果有很多地方都要校验是否有数字,其实就是需要将第一个参数 reg 进行复用,这样别的地方就能够直接调用 hasNumber, hasLetter 等函数,让参数能够复用,调用起来也更方便。

  • (提前确认)提前返回 - 返回接受余下的参数且返回结果的新函数 
var on = function(element, event, handler) {
    if(document.addEventListener) {
        if(element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if(element && event && handler) {
            element.attachEvent('on' + event, handler)
        }
    }
}

var on = (function() {
    if(document.addEventListener) {
        return function(element, event, handler) {
            if(element && event && handler) {
                element.addEventListener(event, handler, false)
            }
        };
    } else {
        return function(element, event, handler) {
            if(element && event && handler) {
                element.attachEvent('on' + event, handler)
            }
        };
    }
})();

// 换一种写法可能比较好理解一点;上面就是把 isSupport 这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
    isSupport = isSupport || document.addEventListener
    if(isSupport) {
        return element.addEventListener(event, handler, false)
    } else {
        return element.attachEvent('on' + event, handler)
    }
}

我们在做项目的过程中,封装一些 dom 操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对于第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

  • 延迟执行 - 返回新函数,等待执行
Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
    // Array.prototype.slice.call(arguments, 1)解说:
    // slice 常用写法,arrObj.slice(start, end)   start: 必需
    // slice()  从已有的数组中返回选定的元素,
    // Array.prototype.slice.call(arguments, 1)  ==>  arguments.slice(1)
    // call 将 slice() 中的 this 指向了 arguments,并将 strat = 1

    return function() {
        return _this.apply(context, args)
    }
}    

像我们 js 中经常使用的 bind,实现的机制就是 Currying。

 

9、反柯里化

 

10、如何让 (a==1 && a==2 && a==3)的值为 true?

1.利用隐式类型转换

 == 操作符在左右数据类型不一致时,会先进行隐式转换。

  a==1 && a==2 && a==3 的值意味着其不可能是基本数据类型。因为如果 a 是 null 或者是 undefined、bool 类型,都不可能返回 true。

  因此可以推测 a 是复杂数据类型,JS 中复杂数据类型只有 object,回忆一下,Object 转换为原始类型会调用什么方法?

  • 如果部署了 [Symbol.toPrimitive] 接口,那么调用此接口,若返回的不是基本数据类型,抛出错误。
  • 如果没有部署 [Symbol.toPrimitivel] 接口,那么根据要转换的类型,先调用 valueOf / toString

  1. 非 Date 类型对象,hint 是 default 时,调用顺序为:valueOf >>> toString,即 valueOf 返回的不是基本数据类型,才会继续调用 valueOf,如果 toString 返回的还不是基本数据类型,那么抛出错误。

  2. 如果 hint 是 string(Date 对象的 hint 默认是 string),调用顺序为:toString >>> valueOf,即 toString 返回的不是基本数据类型,才会继续调用 valueOf,如果 valueOf 返回的还不是基本数据类型,那么抛出错误。

  3. 如果 hint 是 number,调用顺序为:valueOf >>> toString

// 部署 [Symbol.toPrimitivel] / valueOf / toString 皆可
// 一次返回 1, 2, 3  即可。
let a = {
    [Symbol.toPrimitive]: (function(hint)) {
        let i = 1;
        // 闭包的特性之一:不会被回收
        return function() {
            return i++
        }
    })()
}

 

2. 利用数据劫持(Proxy / Object.definedProperty)
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;

 

11、什么是 BFC?BFC 的布局规则是什么?如何创建 BFC?

在解释 BFC 之前,我们需要先知道 Box、Formatting Context 的概念(直译:“块级格式化上下文”)。

Box 是 CSS 布局的对象和基本单位,页面是由若干个 Box 组成的。

元素的类型和 display 属性,决定了这个 Box 的类型。不同类型的 Box 会参与不同的 Formatting Context。

 

Formatting Context

 Formatting Context 是页面的一块渲染区域,并且有一套渲染规则,决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

Formatting Context 有 BFC(Block formatting context),IFC(Inline formatting context),FFC(Flex formatting context)和 GFC(Grid formatting context)。FFC 和 GFC 为 CC3 中新增。

 

BFC布局规则
  • BFC 内,盒子依次垂直排列。
  • BFC 内,两个盒子的垂直距离由 margin 属性决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠【符合合并原则的 margin 合并后是使用大的 margin 】
  • BFC 内,每个盒子的左外边缘接触内部盒子的左边缘(对于从右到左的格式,右边缘接触)。即使在存在浮动的情况下也是如此。除非创建新的 BFC。
  • BFC 的区域不会与 float box 重叠。
  • BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  • 计算 BFC 的高度时,浮动元素也参与计算。

 

如何创建 BFC
  • 根元素
  • 浮动元素(float 属性不为 none)
  • position 为 absolute 或 fixed
  • overflow 不为 visible 的块元素
  • display 为 inline-block,table-cell,table-caption

 

BFC  的应用

 1.  防止 margin 重叠(同一个 BFC 内的两个相邻的 Box 的 margin 会发生重叠,触发生成两个 BFC,即不会重叠)

2.  清除内部浮动(创建一个新的 BFC,因为根据 BFC 的规则,计算 BFC 的高度时,浮动元素也参与计算)

3.  自适应多栏布局(BFC 的区域不会与 float box 重叠。因此,可以触发生成一个新的 BFC)

 

12、异步加载 JS 脚本的方式有哪些?

<script> 标签中增加 async(html5)或者 defer(html4)属性,脚本就会异步加载。

 <script src=".../xxx.js" defer></script>

 

defer 和 async 的区别在于:

  • defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在window.onload 之前执行;
  • async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
  • 如果有多个 defer 脚本,会按照它们在页面出现的顺序加载
  • 多个 async 脚本不能保证加载顺序

 

动态创建 script 标签

 动态创建的 script,设置 src 并不会开始下载,而是要添加到文档中,JS 文件才会开始下载。

let script = document.createElement('script')
script.src = 'xxx.js'
// 添加到 html 文件中才会开始下载
document.body.append(script);

 

XHR 异步加载 JS
let xhr = new XMLHttpRequest();
xhr.open('get', 'js/xxx.js', true);
xhr.send()
xhr.onreadystatechange = function() {
    if(xhr.readyState == 4 && xhr.status == 200) {
        eval(xhr.responseText);
    }
}

 

13、解决跨域的几种方式

 

14、 ES5 有几种方式可以实现继承?分别有哪些优缺点?

ES5 有 6 种方式可以实现继承,分别为:

1.  原型链继承

原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

function SuperType() {
    this.name = 'yy'
    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.age
}

SubType.prototype.constructor = SubType;

console.log('sub-->', SubType.prototype)
/* SuperType{ --> SubType.prototype
    name: 'yy'
    colors: ['pink', 'blue', 'green', 'yellow']
    getAge: f()
    constructor: f SubType()
        length: 0
        name: 'SubType'
        arguments: null
        caller: null
        prototype: SuperType
            name: 'yy'
            colors: ['pink', 'blue', 'green', 'yellow']
            getAge: f()
            constructor: SubType()
} */
console.log('sub22-->', SubType)
/* f SubType() { --> SubType
    this.age = 22
}
*/

let instancel = new SubType()
instancel.colors.push('yellow')

console.log(instancel.getName()); // yy
console.log(instancel.colors);     // ['pink', 'blue', 'green', 'yellow']

let instance2 = new SubType()
console.log(instance2.colors); // ['pink', 'blue', 'green', 'yellow']
缺点:

 1. 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享;

2. 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。

 

2.  借用构造函数

 借用构造函数的技术,其基本思想为:

在子类型的构造函数中调用超类型构造函数。

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green']
}

function SubType(name){
    SuperType.call(this, name)
}

let instance1 = new SubType('yy')
instance1.colors.push('yellow')
console.log(instance1.colors) // ['pink', 'blue', 'green', 'yellow']

let instance2 = new SubType('Jack')
console.log(instance2.colors) // ['pink', 'blue', 'green']
优点:

1. 可以向超类传递参数

2. 解决了原型中包含引用类型值被所有实例共享的问题

缺点:

 1. 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。

 

3.  组合继承(原型链 + 借用构造函数)

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

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。

 

function SuperType(name){
    this.name = name
    this.colors = ['pink', 'blue', 'green']
}

SuperType.prototype.sayName = function() {
    console.log(this.name)
}

function SuberType(name, age){
    SuperType.call(this, name)  // 第一次调用:在子类型构造函数内部调用
    this.age = age;
}

SuberType.prototype = new SuperType() // 第二次调用:在创建子类型原型的时候调用
SuberType.prototype.constructor = SuberType
SuberType.prototype.sayAge = function() {
    console.log(this.age)
}

let instance1 = new SuberType('yy', 20) 
instance1.colors.push('yellow')
console.log(instance1.colors) // ['pink', 'blue', 'green', 'yellow']
instance1.sayName() // yy  调用的是 SuperType.prototype.sayName

let instance2 = new SuberType('Jack', 22)
console.log(instance2.colors) // ['pink', 'blue', 'green']
instance2.sayName() // Jack

 

缺点:

 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

 

优点:
  •  可以向超类传递参数
  • 每个实例都有自己的属性
  • 实现了函数复用

 

4.  原型式继承

圆形继承 的基本思想:

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(o) {
    function F() { }
    F.prototype = o
    return new F()
}

在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。

缺点:

 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

 

5.  寄生式继承

寄生式继承是与原型式继承紧密相关的一条思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function createAnother(original) {
    var clone = object(original); // 通过调用函数创建一个新对象
    clone.sayHi = function () { // 以某种方式增强这个对象
        console.log('hi')
    };
    return clone; // 返回这个对象
}

var person = {
    name: 'yy',
    hobbies: ['reading', 'photography']
}

var person2 = createAnother(person)
person2.sayHi() //hi

基于 person 返回了一个新对象 —— person2,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

缺点:
  •  使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率底下。
  • 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

 

6.  寄生组合式继承

 所谓寄生组合式继承,即通过借用构造函数来继承属性,又通过原型链的混成形式来继承方法,基本思路:

不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType; // 增强对象
    subType.prototype = prototype; // 指定对象
}
  • 第一步:创建超类型原型的一个副本
  • 第二步:为创建的副本添加 constructor 属性
  • 第三步:将新创建的对象赋值给子类型的原型

至此,我们就可以通过调用 inheritPrototype 来替换为子类型原型赋值的语句:

function SuperType(name) {
    this.name = name
    this.colors = ['pink', 'blue', 'green']
}

// ...code
function SuberType(name, age){
    SuperType.call(this, name)
    this.age = age
}

// SuberType.prototype = new SuperType()
inheritPrototype(SuberType, SuperType)
优点:

 只调用了一次超类构造函数,效率更高。避免在 SuberType.prototype 上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。

因此寄生组合继承是引用类型最理性的继承方式。

 

15、隐藏页面中的某个元素的方法有哪些?

隐藏类型:

屏幕并不是唯一的输出机制,比如说屏幕上看不见的元素(隐藏的元素),其中一些依然能够被读屏软件阅读出来(因为读屏技术依赖于可访问性树来阐述)。为了消除它们之间的歧义,我们将其归为三大类:

完全隐藏:元素从渲染树种消失,不占据空间。

视觉上的隐藏:屏幕中不可见,占据空间。

语义上的隐藏:读屏软件不可读,占据空间。

 

完全隐藏

 1. display 属性

display: none

2. hidden 属性

HTML5 新增属性,相当于 display: none

<div hidden></div>

 

视觉上的隐藏

1. 利用 position 和 盒模型 将元素移出可视区范围

1)利用 position 为 absolute 或者 fixed ,通过设置 top、left 等值,将其移出可视区域。

position: absolute;
left: -99999px;

2)设置 position 为 relative ,通过设置 top、left 等值,将其移出可视区域。

position: relative;
left: -99999px;
height: 0;

3)设置 margin 值,将其移出可视区域范围(可视区域占位)。

margin-left: -99999px;
height: 0

2. 利用 transform

1)缩放

transform: scale(0);
height: 0

2)移动 translateX,translateY

tranform: translateX(-99999px);
height: 0

3)旋转 rotate

tranform: rotate(90deg)

3. 设置其大小为 0

1)宽高为 0,字体大小为 0

width: 0;
height: 0;
font-size: 0

2)宽高为 0,超出隐藏

width: 0;
height: 0;
overflow: hidden

4. 设置透明度为 0

opacity: 0

5. visibility 属性

visibility: hidden

6. 层级覆盖,z-index 属性

position: relative;
z-index: -999

再设置一个层级较高的元素覆盖在此元素上。

7. clip-path 裁剪

clip-path: polygon(0 0, 0 0, 0 0, 0 0)

 

语义上的隐藏

 读屏软件不可读,占据空间,可见。

<div aria-hidden="true"></div>

 

16、let、const、var 的区别有哪些?

1)let / const 声明的变量不会出现变量提升,而 var 声明的变量会提升

2)在相同作用域下,let / const 不允许重复声明,而 var 允许重复声明

3)const 声明变量时,必须设置初始值

4)const 声明一个只读的变量,一旦声明,不可更改。

这里有个非常重要的点就是:在 JS 中,复杂数据类型,存储在栈中的是 堆内存 的地址,存在栈中的这个地址是不变的,但是存在堆中的值是可以变得。有没有相当常量指针 / 指针常量

const a = 20
const b = {
    age: 19,
    star: 800
}
b.age = 22

一图胜万言,如下图所示,不变的是栈内存中 a 存储的 20,和 b 中存储的 0x0012ff21(瞎编的一个数字)。而 {age: 19, star: 800} 中的值是可变的。

 

17、说一说你对 JS 执行上下文栈 和 作用域链 的理解

在开始说明 JS 上下文栈和作用域链之前,我们先说明下 JS 上下文以及作用域的概念。

 

JS 执行上下文

执行上下文就是当前 JS 代码被解析和执行时所在环境的抽象概念,JS 中运行任何的代码都是在执行上下文中执行。

 

执行上下文类型分为:
  • 全局执行上下文
  • 函数执行上下文

执行上下文创建过程中,需要做以下几件事:

  1. 创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明。
  2. 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
  3. 确定 this 的值,即 ResolveThisBinding

 

作用域

  作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。—— 摘录自《你不知道的 JavaScript》(上卷)

 

  作用域有两种工作模型:词法作用域 和 动态作用域,JS采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。( with 和 eval 能够修改词法作用域,但是不推荐使用,对此不做特别说明)

 

作用域分为:

  • 全局作用域
  • 函数作用域
  • 块级作用域

 

JS执行上下文栈(后面简称执行栈)

执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。

规则如下:

  • 首次运行 JavaScript 代码的时候,会创建一个全局执行的上下文并 push 到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并 push 到当前执行栈的栈顶。
  • 当栈顶的函数运行完成后,其对应的函数执行上下文将从执行栈中 pop 出,上下文的控制权将移动到当前执行栈的下一个执行上下文。 

 

以一段代码具体说明:

function fun3(){
    console.log('fun3')
}

function fun2(){
    fun3()
}

function fun1(){
    fun2()
}

fun1()

GlobalExecutionContext(即全局执行上下文)首先入栈,过程如下:

伪代码:

// 全局执行上下文首先入栈
ECStack.push(globalContext);

// 执行 fun1()
ECStack.push(<fun1> functionContext)

// fun1 中又调用了 fun2
ECStack.push(<fun2> functionContext)

// fun2 中又调用了 fun3
ECStack.push(<fun3> functionContext)

// fun3 执行完毕
ECStack.pop()

// fun2 执行完毕
ECStack.pop()

// fun1 执行完毕
ECStack.pop()

// javascript 继续顺序执行下面的代码,但 ECStack 底部始终有一个 全局上下文 (globalContext)

 

作用域链

  作用域链就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没有找到,就宣布放弃。这种一层一层的关系,就是作用域链。如:

var a = 10
function fn1(){
    var b = 20;
    console.log(fn2)
    function fn2(){
        a = 20
    }
    return fn2
}
fn1()()

fn2 作用域链 = [ fn2作用域,fn1作用域,全局作用域 ]

18、防抖函数的作用是什么?请实现一个防抖函数

防抖函数的作用:

  防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着 N 秒内函数只会被执行一次,如果 N 秒内再次被触发,则重新计算延迟时间

 

举例说明:小思最近在减肥,但是她非常喜欢吃零食。为此,与其男朋友约定好,如果 10 天不吃零食,就可以购买一个包(不要问为什么是包,因为包治百病)。但是如果中间吃了一次零食,那么就要重新计算时间,直到小思坚持 10 天没有吃零食,才能购买一个包。所以,管不住嘴的小思,没有机会买包... 这就是防抖

 

防抖函数实现

  1. 事件第一次出发时,timer 是 null,调用 later(),若 immediate 为 true,那么立即调用 func.apply(this, params);如果 immediate 为 false,那么过 wait 之后,调用 func.apply(this, params)
  2. 事件第二次触发时,如果 timer 已经重置为 null(即 setTimeout 的倒计时结束),那么流程与第一次触发时一样,若 timer 不为 null(即 setTimeout 的倒计时未结束),那么清空定时器,重新开始计时。
function debounce(func, wait, immediate = true) {
    let timer, result;
    
    // 延迟执行函数
    const later = (context, args) => setTimeout(() => {
        timer = null; //倒计时结束
        if(!immediate){
            // 执行回调
            result = func.apply(context, args);
            context = args = null
        }
    }, wait)

    let debounced = function(...params) {
        if(!timer){
            timer = later(this, params);
            if(immediate){
                // 立即执行
                result = func.apply(this, params)
            }
        } else {
            clearTimeout(timer)
            // 函数在每个等待时延的结束被调用
            timer = later(this, params)
        }

        return result
    }

    // 提供在外部清空定时器的方法
    debounced.cancel = function () {
        clearTimeout(timer)
        timer = null
    }
       
    return debounced
}
window.addEventListener("resize",debounce(handle,1000));
 

immediate 为 true 时,表示函数在每个等待延时的开始被调用。immediate 为 false 时,表示函数在每个等待延时的结束被调用。

 

防抖函数的简单实现

function debounce(fn,wait){
    var timer = null;
    return function(){
        if(timer !== null){
            clearTimeout(timer);
        }
        timer = setTimeout(fn,wait);
    }
}
    
function handle(){
    console.log(Math.random());
}
    
window.addEventListener("resize",debounce(handle,1000));

 

防抖的应用场景

1. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。

2. 表单验证

3. 按钮提交事件。

4. 浏览器窗口缩放,resize 事件(如窗口停止改变大小之后重新计算布局)等。

 

http1.0 与 http 2.0 的区别

http 与 https 的区别

 

posted on 2021-12-15 16:49  bala001  阅读(52)  评论(0编辑  收藏  举报

导航