# 🌵 JavaScript

# 001: JS 中的数据类型有哪些,区别是什么?

  1. 原始类型有(可以用 typeof 检查):Boolean 、String 、Number 、Undefined 、Null 、Symbol(ES6) 、BigInt(ES2020)

  2. 对象类型有:对象 Object(包含普通对象 Object 、数组 Array 、函数 Function 、正则 RegExp 、日期 Date 、数学函数 Math )

  3. 不同之处:原始类型储存值,对象类型储存地址。

  4. 注意点:
    (1)闭包变量存储在堆内存;
    (2)typeof null -> object,但 null 严谨来说不是对象,是历史遗留的 bug,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object;
    (3)number 是浮点类型,浮点运算时会有 bug。
    (4)BigInt 是一种新的数据类型,用于当整数值大于 Number 数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数 id,等等,而不需要使用库。

    9007199254740992 === 9007199254740993; // true (Number的最大安全数为2^53-1)
    9007199254740992 === 9007199254740993n; // false (要创建BigInt,只需要在数字末尾追加n即可)
    

# 0.1 + 0.2 === 0.3 打印什么

false。

JavaScirpt 使用 Number 类型来表示数字(包括整数或浮点数),遵循 IEEE 754 标准 通过 64 位来表示一个数字。

第 0 位:符号位 a,0 表示正数 ,1 表示负数;第 1 位到第 11 位:储存指数部分 e;第 12 位到第 63 位:储存小数部分(即有效数字)f。

二进制表示有效数字总是 1.xx…xx 的形式,有效数字尾数部分 f 在规约形式下第一位默认为 1(省略不写,xx..xx 为尾数部分 f,最长 52 位)。因此,JavaScript 提供的有效数字最长为 53 个二进制位(64 位浮点的后 52 位+被省略的 1 位)因此 js 最大安全数是 Number.MAX_SAFE_INTEGER === Math.pow(2,53) - 1, 而不是 Math.pow(2,52) - 1

在两数相加时,计算机无法直接对十进制的数字进行运算,这是硬件物理特性已经决定的。先按照 IEEE 754 转成相应的二进制,然后对阶运算

0.1 和 0.2 转换成二进制的时候尾数会发生无限循环,且由于 IEEE 754 尾数位数限制,需要将后面多余的位截掉;

由于指数位数不相同,运算时需要对阶运算(两个进行运算的浮点数的阶码对齐,使两个浮点数的尾数能够对齐进行加减运算),JS 引擎对二进制进行截断,所以造成精度丢失。

(本质是 二进制模拟十进制进行计算时 的精度问题。)

相关文章:0.1 + 0.2 不等于 0.3 (opens new window)

# 为什么 x=0.1 能得到 0.1?

这是因为这个 0.1 并不是真正的 0.1,JS 最大可以表示 2^53(9007199254740992), 长度是 16,所以可以使用 toPrecision(16) 来做精度运算。超过的精度会自动做凑整处理。

0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1

// 但来一个更高的精度:
0.1.toPrecision(21) = 0.100000000000000005551

# 怎么解决精度问题?

(1)将数字转成整数

function add(num1, num2) {
    const num1Base = num1.split(".")[1].length || [].length;
    const num2Base = num2.split(".")[1].length || [].length;

    const parse = Math.pow(10, Math.max(num1Base, num2Base));
    return (num1 * parse + num2 * parse) / parse;
}

(2)三方库 - Math.js

# JS 整数是怎么表示

通过 Number 类型来表示,遵循 IEEE754 标准,通过 64 位来表示一个数字,(1 + 11 + 52),最大安全数字是 Math.pow(2, 53) - 1,对应是 16 位十进制。(符号位 + 指数位 + 小数部分有效位)

Number() 的存储空间是 Math.pow(2, 53) ,53 为有效数字,等于 JS 能支持的最大数字。如果接收到一个超过范围的数字会自动被截断。

# 002: 请写出如下代码的打印结果。

let name = "chieminchan";

function changeTest(name) {
    name = "kate";
}
changeTest(name);

console.log(name); // -> chieminchan

// 这里把 name 变量对应的值 chieminchan 传入函数 changeTest,然后复制给了的一个局部变量,改变局部变量的值不会对外部变量产生影响,因此打印的依然的是 chieminchan
const person1 = {
    name: "chieminchan",
    age: 20,
};

function changeTest(person) {
    person.age = 22;
    person = {
        name: "jack",
        age: 24,
    };

    return person;
}

const person2 = changeTest(person1);

console.log(person1); // -> ?
console.log(person2); // -> ?

这里看似成了地址传递,但其实这一段和上一段 js 代码的函数传参方式都是值传递。

此处将 person1 对应的值传入了 changeTest 函数,同样地将这个值复制了一个副本给函数里面的局部变量,只不过这个局部变量的值是指向堆内存中的地址。这时候,在函数内部对这个局部变量的属性进行修改,就变成了对 这个局部变量 所指向的堆内存地址 的值进行修改。

因为外部变量 person1 的值也是指向同一个堆内存的地址,因此外部变量的值也一并发生了变化。

console.log(person1); // -> { name: 'chieminchan', age : 22 }
console.log(person2); // -> { name: 'jack', age: 24 }

考察点:ECMAScript 所有的函数的参数都是按值传递,即函数参数传递的是变量的值。

# 003: 列出判断数据类型的方法和区别。

# (1)常用的 typeof

对于原始类型来说,除了 null,typeof 可以正确显示对应的数据类型:

typeof 1; // 'number'
typeof "1"; //'string'
typeof True; // 'boolean'
typeof undefined; //'undefined'
typeof Symbol(); // 'symbol'

typeof new String("A"); //"object",new String会返回一个对象
typeof String("A"); //"string",String 会返回一个字符串

但对于对象类型来说,除了函数,都会显示object,因此typeof不能准确地对数据类型进行判断:

typeof []; // 'object'
typeof {}; //'object'
typeof function() {}; // 'function'

// 特殊例子
typeof null; //'object'
typeof Object; // 'function'
typeof Object(); // 'object'

# (2)instanceof(内部机制是通过原型链来判断,一般用来判断对象类型)

const Person = function() {};
const p1 = new Person();
p1 instanceof Person; // true

const str1 = new String("hello world");
str1 instanceof String; // true

const a = ["1", "2"];
a instanceof Array; //true;
a instanceof Object; //true;

null instanceof object; //false;

# (3) Object.prototype.toString.call

Object.prototype.toString.call(function() {}); // "[object Function]";
Object.prototype.toString.call([]); // "[object Array]"

Object.prototype.toString.call("1"); // "[object String]"
Object.prototype.toString.call(new String(1)); // "[object String]"
Object.prototype.toString.call(2); //"[object Number]"
Object.prototype.toString.call(null); //"[object Null]"

参考文章:
浅谈 instanceof 和 typeof 的实现原理 (opens new window)

# typeof 实现原理

typeof 是通过获取数据底层的类型标签来获取类型信息。

JS 在底层存储变量的时候,会在变量的机器码的低位 1-3 位存储其类型信息:

000:对象
010:浮点数
100:字符串
110:布尔
1:整数

对于 undefinednull 来说,这两个值的信息存储是有点特殊,null:所有机器码均为 0,undefined:用 −2^30 整数来表示。

typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为 0,因此直接被当做了对象来看待。

# instanceof 实现原理

instanceof 的实现原理:

instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。

因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

# instanceof 手写实现

function _instanceof(instance, right) {
    const rightProto = right.prototype;
    const instanceProto = instance.__proto__;

    while (true) {
        if (instanceProto === rightProto) {
            return true;
        }

        if (instanceProto === null) {
            return false;
        }

        instanceProto = instanceProto.__proto_;
    }
}

# 顺带引出一些例子

// Function Object都是由函数构造
Function instanceof Function; // true
Function instanceof Object; // true

//Object.__proto__ = Object.prototype
// Object.prototype为一个对象,
Object instanceof Object; // true
Object instanceof Funciton; // true(对象由函数创建)

function Foo() {}
Foo instanceof Foo; // false
Foo instanceof Function; // true
Foo instanceof Object; // true

原型图

# 但对于原始类型不能直接通过 instanceof 来判断类型,特殊例子:

var str = "hello world";
str instanceof String; // false

let str2 = new String("abc");
typeof str2; // "object"
str2 instanceof String; // true

需要利用Symbol.hasInstance (opens new window)将原有的 instanceof 方法重定义,换成了 typeof,才能能够判断基本数据类型:

class PrimitiveNumber {
    static [Symbol.hasInstance](x) {
        return typeof x === "number";
    }
}
console.log(111 instanceof PrimitiveNumber); // true

# 004: == 和 ===有什么区别?

=== 为严格相等,而 == 比 === 条件更宽松。
=== 要求两边的值和类型都要相同, == 在比较时只要求值相同。

== 比较值的规则:
(1)首先会判断两者类型是否相同。相同的话就是比大小了;类型不相同的话,那么就会进行类型转换:
(2)会先判断是否在对比 null 和 undefined,是的话返回 true ;

null == undefined -> true

(3)判断两者类型是否为 string 和 number,是的话就会将字符串 转换为 Number

1 == '1' -> 1 ==  1 -> true

(4)判断其中一方是否是 Boolean,是的话就把 Boolean 转换成 Number,再进行比较;

'1' == true   ->  '1' ==  1 -> 1  ==  1 -> true

PS: 请记住 6 个虚值(null,undefined,'',0,NaN,false) -> 转成布尔都会是 false

(5)如果其中一方为 Object,且另一方为 String、Number 或者 Symbol,会将 Object 转换成 原始类型,再进行比较。
(PS:这里会优先通过 valueOf 进行转换,如果 valueOf 输出的值类型是引用类型,将会通过 toString 进行转换)

{ a: 1 } == 1
->  '[object Object]' == 1
-> NaN == 1
-> false

[] == ![]
 -> [] == false (!的优先级是大于 == 的,先执行后者)
 -> [] == 0 (按照第(4)点来,其中一方是 Boolean 要先转换成 Number)
 -> '' == 0 (按照第(5)点来,一方为Object,需valueOf转成原始类型)
 ->  0 == 0 (按照第(3)点来,''转数字,Number('')值为0)
 -> true

//如果两个操作数都是对象,则比较它们是不是同一个对象引用,如果两个操作数都指向同一个对象
[] == {} -> false

// 验证(5)
const test = { a: 1 };
test.valueOf = () => {
  console.log('valueOf');
  return this;
}
test.toString = () => {
  console.log('toString');
  return '1'
}
console.log(test == 1) // valueOf  toString  true

(更详细规则详情戳这里 (opens new window))

# 加号的隐形转换

在加号中,会使用 ToPrimitive 运算转换左与右运算元为原始数据类型值(primitive)

在第 1 步转换后,如果有运算元出现原始数据类型是"字符串"类型值时,则另一运算元作强制转换为字符串,然后作字符串的连接运算(concatenation)

在其他情况时,所有运算元都会转换为原始数据类型的"数字"类型值,然后作数学的相加运算(addition)

[]+{} ={}+[] =

引用知乎的回答:https://www.zhihu.com/question/45478070?sort=created:

  • 先说 [] + {}
    一个数组加一个对象。

    加法会进行隐式类型转换,规则是调用其 valueOf() 或 toString() 以取得一个非对象的值(primitive value)。

    如果两个值中的任何一个是字符串,则进行字符串串接,否则进行数字加法。

    [] 和 {} 的 valueOf() 都返回对象自身(一个空数组和一个空对象),所以都会调用 toString(),最后的结果是字符串串接。

    [].toString() 返回空字符串,({}).toString() 返回“[object Object]”。最后的结果就是“[object Object]”。

  • {} + []
    {} 除了表示一个对象之外,也可以表示一个空的 block。

    {} + [] 中,如果{}(空对象)在前面,而[](空数组)在后面时,前面(左边)那个运算元会被认为是区块语句而不是对象字面量。

    所以{} + []相当于+[]语句,也就是相当于强制求出数字值的 Number([])运算,相当于 Number("")运算,最后得出的是 0 数字。

    {} 被解析为空的 block,随后的 + 被解析为正号运算符。即实际上被解析成成了:{ // empty block }

    +[] 即对一个空数组执行正号运算,实际上就是把数组转型为数字。首先调用 [].valueOf(),返回数组自身,不是 primitive value,因此继续调用 [].toString() ,返回空字符串。空字符串转型为数字,返回 0,即最后的结果。

# 005: 如何理解作用域链,作用域链的尽头是什么?(针对变量)

(1)作用域是定义变量的区域,可确定当前执行代码对变量的访问权限,同时也决定了变量的生命周期。Javascript 采用的是词法作用域(就是静态作用域),函数的作用域基于函数创建的位置,函数一旦创建了就会产生一个作用域,函数内部会有一个 [scope] 属性;

(2)作用域链可以理解为在函数执行时,用于搜索变量的一条链子。当在一个函数中访问某个变量时,会先在当前函数上下文的变量对象中作用中检索,如果没检索到这个变量,逐步往上一级的变量对象检索,一直到全局上下文的变量对象(浏览器环境下全局对象 window)。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

var value = 1;
function bar() {
    var value = 2;

    function foo() {
        console.log(value);
    }

    return foo();
}
bar(); // 2

执行函数 foo,因为 foo 的创建位置在 bar 函数体内,因此在 foo 自身作用域搜寻不到变量 value 时,会往上一层搜寻,即 bar 函数体内,找到 value 为 2,因此结果输出 2。

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar(); // 1

因为是静态作用域,执行 foo 函数时候会根据创建的位置,先从自身的局部作用域搜寻变量 value,找不到就往上搜寻,于是 value 为 1 ,结果也会输出 1。

再来个例子,猜猜输出什么:

function bar() {
    var myName = " 极客世界 ";
    let test1 = 100;
    if (1) {
        debugger;
        let myName = "Chrome 浏览器 ";
        console.log("a", test);
    }
}
function foo() {
    var myName = " 极客邦 ";
    let test = 2;
    {
        let test = 3;
        bar();
    }
}
var myName = " 极客时间 ";
let myAge = 10;
let test = 1;
foo();

# 006: 什么是闭包,为什么要用它?

# 对闭包的理解

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。当通过调用一个外部函数 A 返回一个内部函数 B 后,即使该外部函数 A 已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

如:

function f1() {
    var a = 2;
    function f2() {
        console.log(a); //2
    }
    return f2;
}
var x = f1();
x(); // 2

# 闭包产生的原因

由于作用域链从最底层向上找,从当前作用域找到全局作用域 window 为止。如果全局还没有的话就会报错。因此,为了在当前环境中能存在指向局部作用域的引用,可以间接访问函数内部的变量,闭包就出现了。

# 常见的闭包使用方式

  • (1) 中作为函数返回值被返回;

  • 作为函数参数传递;

    var a = 1;
    function test1() {
        var a = 2;
    
        // 闭包
        function test2() {
            console.log(a);
        }
        test3(test2);
    }
    
    function test3(fn) {
        // 闭包
        fn();
    }
    
    test1(); // 2
    
  • 在定时器、事件监听、Ajax 请求或者任何异步中,只要使用了回调函数,实际上就是在使用闭包;

    // 事件监听
    $("#app").click(function() {
        console.log("DOM Listener");
    });
    
    // 定时器
    setTimeout(function() {
        console.log("in settiomeout");
    }, 0);
    
  • IIFE(立即执行函数表达式)创建闭包。

    for (var i = 1; i <= 5; i++) {
        setTimeout(function timer() {
            console.log(i);
        }, 0);
    }
    

    按照 js 事件循环的机制,setTimeout 是宏任务,等待主线程的循环任务执行完毕,接着会执行宏任务 setTimeout 的回调,因为在回调函数的局部作用域中没有变量 i,因此会依次往上找到全局变量 i,因为此时循环已结束,所以 i 已变成了 6,所以会全部输出 6。

    可以利用 IIFE 来解决这个问题,当每次 for 循环时,把此时的 i 值作为参数传递到定时器中:

    for (var i = 1; i <= 5; i++) {
        (function(j) {
            setTimeout(function timer() {
                console.log(j);
            }, 0);
        })(i);
    }
    

    (另外,这个问题的推荐用 es6 的 let 来解决。)

# 闭包的回收机制

如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

# 007: 原型和构造函数的关系,如何理解原型链?(针对对象属性)

(1)原型
每个函数数据类型内部都会默认有一个 prototype 属性,这个属性指向函数的原型对象。

而这个原型对象就是这个函数作为构造函数时,通过 new 创建的实例对象的原型。(实例对象通过隐式的 __proto__ 指向构造函数的原型对象)

可以说在 new 的过程中,新对象就会被添加了__proto__ 并且链接到构造函数的原型对象上。)

(2)原型和构造函数的关系,constructor

实例对象和构造函数可分别通过__proto__prototype指向原型,而原型可通过constructor属性指向构造函数。(让实例对象知道是什么函数构造了它)

原型图

如上图所示:
1)构造函数 Person 的实例对象为 person,Person 通过 prototype 指向自己的原型对象 Person.prototype
2)原型对象 Person.prototype 可通过constructor指向构造函数 Person;
3)实例 person 通过隐式的 __proto__ 指向原型 Person.prototype;
4)Person.prototype 原型对象同时也是一个对象,即作为 Object 的实例,因此也有Person.prototype.__ proto__ 属性指向自己的原型 Object.prototype。

(3)原型链
原型链就是多个对象通过__proto__连接起来的一个链子。

在实例对象里面,如果要访问某个属性,首先会从这个实例对象内部搜寻;如果在内部找不到这个属性,就会去它的原型里面找这个属性,若还是没有,就沿着原型链,向它的原型的原型,直到找到为止。

原型链最顶层是 Object 的原型对象 Object.prototype ,而它的原型是 null,因此到此便会终止搜寻。

原型图

⚠️ :
a. 对象的 hasOwnProperty() 可用于检查对象自身中是否含有该属性;
b. 使用 in 也可检查对象中是否含有某个属性,如果对象中没有,但是原型链中有,也会返回 true。

相关题:

function Ofo() {}

function Bick() {
    this.name = "bick";
}

Ofo.prototype = new Bick();
var OfoBick = new Ofo();
var bick = new Bick();

console.log(OfoBick.name);
console.log(bick.name);

// bick
// bick

# Object.create 的实现

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}

Person.prototype.sayHi = function() {
    console.log(`hi,i am ${this.name}`);
};

var b = Object.create(Person.prototype, {
    name: {
        value: "coco",
        writable: true,
        configurable: true,
        enumerable: true,
    },
    sex: {
        enumerable: true,
        get: function() {
            return "hello sex";
        },
        set: function(val) {
            console.log("set value:" + val);
        },
    },
});

// 以上其实等价于
// var c = new Person("coco", "boy");

Object.create(proto,[propertiesObject])
proto:新创建对象的原型对象
propertiesObject:可选。要添加到新对象的可枚举的属性,包括属性名字属性值以及描述符(新添加的属性是其自身的属性,而不是其原型链上的属性,可用 hasOwnProperty()获取)

Object.create(null) 和 {} 的区别:前者会创建一个纯净不继承 Object 原型链上方法的全空白对象,{}创建的对象会继承 Object 原型链上方法

由以上可知,Object.create 做的事情主要有:
1)创建一个新的对象,并以第一个参数作为新对象的proto属性的值(以第一个参数作为新对象的构造函数的原型对象)
2)有第二个可选参数,是一个对象,对象的每个属性都会作为新对象的自身属性,对象的属性值以 descriptor(Object.getOwnPropertyDescriptor(obj, 'key'))的形式出现,且 enumerable 默认为 false

// step1: 暂时不考虑第二个参数
Object.prototype._create1 = function(proto) {
    //实现一个隐藏函数
    function F() {}

    //函数的原型设置为参数传进来的原型
    F.prototype = proto;
    // F.prototype.constructor = F;

    // 返回一个F函数的实例,即此实例的__proto__指向为参数proto
    const newObj = new F();

    // 传进来的原型为null时,建立一个全空对象
    if (proto === null) {
        newObj.__proto__ = null;
    }

    return newObj;
};

// step2,考虑第二个参数
Object.prototype._create2 = function(proto, properties = null) {
    function F() {}
    F.prototype = proto;

    const newObj = new F();

    if (properties) {
        // Object.defineProperty定义新属性或修改原有的属性
        // Object.defineProperty(obj, prop, descriptor)
        // Object.defineProperty(obj, "test", {
        //     value: 任意类型的值,
        //     configurable: true | false, // 是否可删除/再次设置特性
        //     enumerable: true | false, //是否可枚举(使用for...in或Object.keys())
        //     writable: true | false, //是否可被重写
        // });
        Object.defineProperties(newObj, properties);
    }

    if (proto === null) {
        newObj.__proto__ = null;
    }

    return newObj;
};

# 008: new 具体干了些什么?通过 new 的方式创建对象和通过字面量创建有什么区别?模拟实现 new?

(1)new 操作背后的步骤:

  • 创建一个空对象,并把这个空对象的原型指向构造函数原型对象;(继承自构造函数原型对象的实例)
  • 使用 apply 将构造函数的 this 指向空对象,并执行一遍构造函数,为空对象增加新的属性方法;
  • 构造函数有 return 语句,但没有指定返回值,或返回一个原始值,那么这时将忽略返回值,返回这个新对象;若返回的是一个对象,则返回构造函数提供这一个对象。
  • 构造函数无 return,则直接返回这个新对象。

(2) 通过 new 创建对象和字面量创建对象的方式区别:

  • 字面量创建对象不会调用 Object() 构造函数,简洁明了,性能较好;
  • 通过 new Object() 方式创建对象本质上是方法调用,涉及到在原型链中遍历该方法,当找到该方法后,又会生产方法调用必须的堆栈信息,方法调用结束后,还要释放该堆栈,性能会不如字面量的方式。

(3)模拟实现 new:

这里主要参考了大神冴羽 (opens new window)的文章:

function _new() {
    // 简单创建一个空对象
    var obj = {};

    // 获取从外部传入的构造器,即第一个参数
    var constructor = [].shift.call(arguments);

    // 实例指向的原型
    obj.__proto__ = constructor.prototype;

    // 也可通过 Object.create 直接创建继承自构造函数原型对象的实例对象
    // var obj = Object.create(constructor.prototype);

    // 通过改变外部传入的构造器中的this指向,并执行构造器,给obj设置新的属性
    var result = constructor.apply(obj, arguments);

    // 根据构造器的返回值,来决定该返回什么
    // 构造函数有返回,且返回的是一个对象则按构造函数的对象来返回
    // 构造函数无返回,即返回 undefined,则返回这个新创建的对象
    return typeof result === "object" ? result || obj : obj;
}

# Object.create 、{}、new 的区别?

  • 直接字面量创建 {}
    创建一个新对象,并且继承 Object 原型的属性方法。

  • new 关键字创建
    创建一个给定的构造函数原型的新实例对象,实例对象会继承构造函数的属性方法。

  • Object.create()
    创建一个继承给定的第一个参数原型的新对象,第一个参数为 null 时会返回一个真正的空对象。

# 009: call、apply 和 bind 的区别,模拟实现 bind、call?

可戳《call、apply、bind 的实现》

# 010: 对 this 指向的理解

(1)call/apply/bind 显示绑定
(2)全局上下文中,this 默认绑定到 window,严格模式下指向 undefined
(3)谁最后调用就指向谁

let foo = {
    bar: function() {
        console.log(this);
    },
};
const func = foo.bar;
func(); // window

👆 func() 是方法调用,this 指向调用者,即全局上下文的情况。

foo.bar(); // foo

👆 foo.bar() 以对象.方法的形式调用,this 指向调用者,即 foo

⚠️ 值得注意,有个特殊的情况:

let foo = {
    name: "foo",
    bar: {
        name: "bar",
        baz: function() {
            console.log(this.name);
        },
    },
};
let name = "window";
foo.bar.baz(); // bar

若一串对象属性链中的某个属性对应的一个函数中有 this,尽管这个函数是被最外层的对象所调用,this 指向也只会是最内层的对象。

(5)new 绑定
如果函数或者方法调用之前带有关键字 new,就构成构造函数调用。此时的 this 指向构造函数的实例对象。

(6)箭头函数

  • 箭头函数中没有 this 绑定,必须通过查找作用域链来决定 this 的值。

如果箭头函数被非箭头函数包含,则 this 指向当前外层最近的非箭头函数的 this,否则,找不到就是 window(严格模式是 undefined)。

const obj1 = {
    foo: () => {
        console.log(this);
    },
};

obj1.foo(); // window

const obj2 = {
    foo: function() {
        console.log(this);
        const bar = () => {
            console.log(this);
        };
        bar();
    },
};
obj2.foo();
// obj2 {foo: ƒ}
// obj2 {foo: ƒ}

// bar() 找到最近的非箭头函数为 foo,而此时 foo 是被 obj2 调用,即 foo 中的 this 指向 obj2,因此箭头函数中 this 指向 obj2

const obj3 = {
    foo: function() {
        console.log(this);
        const bar = () => {
            console.log(this);
        };
        bar();
    },
};

const baz = obj3.foo;
baz();
// window
// window

// bar() 找到最近的非箭头函数为 foo,而 baz() 是方法调用, 即 baz 中的 this 指向全局上下文,因此箭头函数中 this 指向全局上下文 window

(7)DOM 事件绑定

# 011: 事件流、事件对象、事件处理程序、事件委托

戳我看详细的《事件流和事件处理程序》

# 012: 深拷贝和浅拷贝,哪些栗子?

当目标对象对赋值对象通过直接赋值的方式进行拷贝,这种情况仅能拷贝了地址,改变一方属性值,由于是同一个引用,会导致同个地址对应的属性值都发生变化。

let a = {
    age: 1,
};
let b = a;
a.age = 2;
console.log(b.age); // 2

(1)浅拷贝

浅拷贝会将对象的各个属性进行依次复制,但并不会进行递归复制,即只会赋值目标对象的第一层属性。

对于目标对象第一层为基本数据类型的数据,就是直接赋值;
而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址。

  • Object.assign

Object.assign 会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的依然是属性的引用,因此只是浅拷贝,并不是深拷贝。

const person1 = {
    name: "Kate",
    age: 20,
    habits: {
        first: "running",
        second: "eating",
    },
};

const person2 = Object.assign({}, person1);
person2.name = "Rose";
person2.habits.first = "shopping";

console.log("person1:", person1);
console.log("person2:", person2);

// person1: {name: "Kate", age: 20, habits: {first: "shopping", second: "eating"}}
// person2: {name: "Rose", age: 20, habits: {first: "shopping", second: "eating"}}
  • 运算符 ...
const person1 = {
    name: "Kate",
    age: 20,
    habit: {
        first: "running",
    },
};

const person2 = { ...person1 };
person2.name = "Rose";
person2.habit.first = "reading";

console.log("person1:", person1);
console.log("person2:", person2);

// person1: {name: "Kate", age: 20, habits: {first: "reading"}}
// person1: {name: "Rose", age: 20, habits: {first: "reading"}}
  • array.concat()
const arr1 = [20, { habit: "eating" }];
const arr2 = arr1.concat();

arr2[0] = 22;
arr2[1].habit = "running";
console.log("arr1:", arr1);
console.log("arr2:", arr2);
// arr1:[20, {habit: "running"}]
// arr2:[22, {habit: "running"}]

(2)深拷贝

深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。

  • JSON.parse(JSON.stringify())
const person1 = {
    name: "Kate",
    age: 20,
    habits: {
        first: "running",
    },
};

const person2 = JSON.parse(JSON.stringify(person1));
person2.name = "Rose";
person2.habits.first = "eating";

console.log("person1:", person1);
console.log("person2:", person2);
// person1: {name: "Kate", age: 20, habits: {first: "running"}}
// person2: {name: "Rose", age: 20, habits: {first: "eating"}}

这个方法可以足够覆盖大多数的应用场景,但有一些情况会出现问题,比如:

  • 在遇到函数、 undefined 或者 symbol 的时候,JSON 不能正常的序列化
const person1 = {
    name: "Kate",
    age: 20,
    sex: Symbol("female"),
    habits: undefined,
    jobs: function() {},
};

const person2 = JSON.parse(JSON.stringify(person1));

console.log("person1:", person1);
console.log("person2:", person2);

// person1: {name: "Kate", age: 20, sex: Symbol(female), habits: undefined, jobs: ƒ ()}
// person2: {name: "Kate", age: 20}
  • 不能解决循环引用的对象
// 拷贝person1会出现系统栈溢出,因为出现了无限递归的情况。
const person1 = {
    name: "Kate",
    age: 20,
    habits: {
        first: "eating",
    },
};

person1.friend = person1;
const person2 = JSON.parse(JSON.stringify(person1));

console.log("person1:", person1);
console.log("person2:", person2);

// Uncaught TypeError: Converting circular structure to JSON at JSON.stringify (<anonymous>)

# 013: 什么是防抖和节流?有什么区别?如何实现?

防抖和节流的主要作用都是为了减少函数无用的触发次数,以便解决响应跟不上触发频率导致页面卡顿这类问题,提高性能和避免资源浪费。

但防抖动和节流本质是不一样的,防抖是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

# 防抖 debounce 原理:

在触发触发某个事件后的 n 秒后才能重新执行,若在触发事件的 n 秒内再次触发这个事件,时间 n 将会重新开始计时,直到触发完事件的 n 秒内不再触发事件,这个事件才会执行。(指定时间内只能触发一次事件,否则多次触发会重新计时。)

应用场景:
常用于搜索框输入实时查询、下载按钮

# 节流 throttle 原理:

当持续触发事件时,保证一定时间段内只能执行一次事件处理函数。(指定时间内多次触发时间后,需要间隔一定时间才会执行)

应用场景:
常用于监听浏览器滚动条

# 防抖和节流的手写实现

可戳《防抖和节流》

# 014: 浏览器重绘和回流?

# 015: createDocumentFragment 创建文档碎片节点

<ul id="lists">
    <li class="list">123</li>
    <li class="list">234</li>
    <li class="list">345</li>
    <li class="list">456</li>
    <li class="list">678</li>
    <li class="list">789</li>
</ul>
<div id="text"></div>

<script>
    const lists = document.getElementById("lists");
    const text = document.getElementById("text");

    // 事件委托
    lists.addEventListener("click", function(e) {
        const target = e.target;
        text.innerText = target.textContent;
    });

    // document.createDocumentFragment
    const fragement = document.createDocumentFragment();
    for (let i = 0; i < 20; i++) {
        let li = document.createElement("li");
        li.innerText = i + ". i am a list";
        fragement.appendChild(li);
    }

    lists.append(fragement);
</script>

# 016: for..in 和 for..of 区别

# for (let key in Obj)

  1. 对于对象,遍历可枚举的对象属性,包括 prototype 中的方法名称,但不包括 不可枚举 和 Symbol 类型 的属性名称

  2. 对于数组,枚举索引(index)

for..in对于数组存在一个问题:

const arr = [1, 2, 3, 4];
arr.name = "ccc";

for (let index in arr) {
    console.log(index); // 0, 1, 2, 3, name
}

console.log(arr.length); // 4

# for (let item of Array)

for..of 为了解决 for..in 的问题,只遍历集合本身的元素,可用于 set、map、array 可迭代对象。

const arr = [1, 2, 3, 4];
arr.name = "ccc";

for (let item of arr) {
    console.log(index); // 1, 2, 3, 4
}

# 017:JavaScript 如何实现继承?(不同方式的优缺点)

# 019:JavaScript 的类和 ES6 的 class 区别

# 020:JavaScript 的设计模式

# 021:reduce

https://www.jianshu.com/p/e375ba1cfc47

reduce polyfill

// 简单版
function _reduce(arr = [], fn, initialVal = undefined) {
    // 初始化值
    let startIndex = 1;
    let startVal = arr[0];

    if (initialVal !== undefined) {
        startIndex = 0;
        startVal = initialVal;
    }

    let result = startVal;

    for (let i = startIndex; i < arr.length; i++) {
        // (prev, cur, curIndex, arr) => {}
        result = fn(result, arr[i], i, arr);
    }

    return result;
}

_reduce([1, 2, 3, 4], (prev, cur) => {
    return prev + cur;
});

// 复杂版
Array.prototype.myreduce = function(fn, initialVal = undefined) {
    const sourceArray = this;
    const length = sourceArray.length;

    // 检验数组长度
    if (!length) {
        throw new TypeError("empty array");
    }

    // 检验fn是否为函数
    if (typeof fn !== "function") {
        throw new TypeError(`${fn} is not a function`);
    }

    // 初始化默认值
    let startIndex = 1;
    let startValue = sourceArray[0];

    if (initialVal !== undefined) {
        startIndex = 0;
        startValue = initialVal;
    }

    let result = startValue;

    for (let i = startIndex; i < length; i++) {
        result = fn(result, sourceArray[i], i, sourceArray);
    }

    return result;
};

[1, 2, 3, 4].myreduce((prev, cur) => {
    return prev + cur;
}, 2);

# 022:compose

# 023:数组方法中的 indexOf 和 includes 实现

Array.prototype.indexOf = function(param, start = 0) {
    //如果start大于等于数组长度,此时this[start]越界,返回-1
    if (start >= this.length) {
        return -1;
    }

    //如果start小于0,判断start + this.length
    if (start < 0) {
        start = start + this.length < 0 ? 0 : this.length + start;
    }

    //start处理完毕,开始从start处遍历数组,查找元素下标
    for(let i = start, i < this.length; i++){
        if(this[i] === param) {
            return i;
        }
    }

    // 遍历完也没找到,返回-1
    return -1;
};

Array.prototype.includes = function (param, start = 0) {
        if (start >= this.length) return false;

        if (start < 0) {
            start = start + this.length < 0 ? 0 : start + this.length;
        }

        //start处理完毕后,判断要查找的元素是不是NaN,
        if (Number.isNaN(param)) {
            //param是NaN,从start处开始逐个判断数组中的元素是不是NaN,如果是,返回true
            for (let i = start; i < this.length; i++) {
                if (Number.isNaN(this[i])) return true;
            }
        } else {
            //param不是NaN,使用===比较即可
            for (let i = start; i < this.length; i++) {
                if (this[i] === param) return true;
            }
        }
        return false
    }

# 024:什么是 Symbol,它的使用场景是什么

新的原始数据类型 Symbol 表示独一无二的值。最大的用法是用来定义对象的唯一属性名,Symbol 作为对象属性名时不能用.运算符,要用方括号obj[symbol]

Symbol.for() 可以在全局访问 Symbol。Symbol.for() 类似单例模式,首先会在全局搜索被登记的 Symbol 中是否有该字符串参数作为名称的 Symbol 值,如果有即返回该 Symbol 值,若没有则新建并返回一个以该字符串参数为名称的 Symbol 值,并登记在全局环境中供搜索。

使用场景:
1)Symbol 用来作为对象的属性名,表示唯一的属性名称,防止变量名冲突或;

2) Symbol 不会被常规的方法(除了 Object.getOwnPropertySymbols,Reflect.ownKeys() 外)遍历到,所以可以用来模拟私有变量;更优雅的“对内操作”和“对外选择性输出”;

3)使用 Symbol 来替代常量,防止常量值相等;

const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();

function handleFileResource(resource) {
    switch (resource) {
        case TYPE_AUDIO:
            console.log("TYPE_AUDIO");
            break;
        case TYPE_VIDEO:
            console.log("TYPE_VIDEO");
            break;
        case TYPE_IMAGE:
            console.log("TYPE_IMAGE");
            break;
        default:
            throw new Error("Unknown type of resource");
    }
}

// 当你想要判断的时候只需要把值传进去
handleFileResource(TYPE_VIDEO); // TYPE_AUDIO

4)注册和获取全局 Symbol

let gs1 = Symbol.for("global_symbol_1"); //注册一个全局Symbol
let gs2 = Symbol.for("global_symbol_1"); //获取全局Symbol

gs1 === gs2; // true

5)注册对象迭代器
Symbol.iterator 可以为每一个对象定义默认的迭代器。该迭代器可以被for…of运算符循环使用。

插入一下关于 iterator 的知识:

一个数据结构只要具有 iterator 接口,就可以用 for...of 循环遍历它的成员。Iterator 为各种不同的数据结构提供统一的访问机制。Iterator 其实就是一个具有 next()方法的对象,并且每次调用 next()方法都会返回一个结果对象,这个结果对象有 value 和 done 两个属性。

Symbol.iterator 会返回一个对象,是一个遍历器对象,而作为遍历器对象,其必须具备的特征就是必须具备 next()方法。
for 循环会不断调用 next 。调用 next 函数,返回 value 和 done 两个属性;value 属性返回当前位置的成员,done 属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用 next 方法;当 done 为 true 时,即遍历完成。

var obj = {
    k1: "a",
    k2: "b",
    k3: "c",
};

Object.defineProperty(obj, Symbol.iterator, {
    // 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
    enumberable: false, // 是否可枚举
    configurable: false, // 是否可以配置对象,删除属性
    writable: false, // 是否可以修改对象

    value: function() {
        var _this = this;
        var index = 0;
        var keys = Object.keys(_this);
        return {
            next: function() {
                const done = index < keys.length ? false : true;
                const value =
                    index < keys.length ? _this[keys[index++]] : undefined;
                return { value, done };
            },
        };
    },
});

for (let item of obj) {
    console.log(item); // a b c
}

留一个问题:有没有其他迭代生成器的写法?

yield* (opens new window)

https://cloud.tencent.com/developer/article/1446889

# 025:Object.keys、Object.getOwnPropertyNames、Object.getPropertySymbols

Object.keys:会返回一个给定对象的自身可枚举属性组成的数组,会遍历原型上的属性,数组中属性名的排列顺序和使用for...in循环遍历该对象时返回的顺序一致。

Object.getOwnPropertyNames:返回一个给定对象的所有可枚举或不可枚举的属性名(但不包括 Symbol 值作为名称的属性和原型链上的属性)组成的数组

Object.geyOwnPropertySymbols:返回一个给定对象的所有 Symbol 值的属性(包括不可枚举的 Symbol 值属性)。

var obj = {};
var a = Symbol("a");
var b = Symbol.for("b");
var c = Symbol.for("c");

obj[a] = "SymbolA";
obj[b] = "SymbolForB";
obj[c] = "SymbolForC";
obj["d"] = "d";

console.log(Object.keys(obj)); // ['d']
console.log(Object.getOwnPropertyNames(obj)); // ['d']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(a), Symbol(b), Symbol(c)]

# 026:window.onload 和 DOMContentLoaded

  1. 当 DOMContentLoaded 事件触发时,仅当 DOM 加载完成,HTML 结构被解析完,但不包括样式表,图片,flash。

  2. 当 onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片都已经加载完成了。

# 027:valueOf 和 toString

  • valueOf

valueOf 方法返回指定对象的原始值。

JavaScript 调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。

const a = [1, 2, 3];
const b = { test: 1 };

console.log(a.valueOf()); // [1,2,3]
console.log(b.valueOf()); // {test: 1}
  • toString

toString 返回一个表示该对象的字符串,当对象表示为文本值或以期望的字符串方式被引用时,toString 方法被自动调用。

const a = [1, 2, 3];
const b = { test: 1 };

console.log(a.toString()); //1,2,3
console.log(b.toString()); // [object Object]
  • 区别:valueOf 偏向于运算,toString 偏向于显示。
  1. 使用运算操作符的情况下,valueOf 的优先级高于 toString。
  2. 在进行对象转换时,将优先调用 toString 方法,如若没有重写 toString,将调用 valueOf 方法;如果两个方法都没有重写,则按 Object 的 toString 输出。
  3. 在进行强转字符串类型时优先调用 toString 方法强转为数字时优先调用 valueOf

共同的缺点:无法获取 null 和 undefined 的值

class A {
    valueOf() {
        console.log("valueOf");
        return 2;
    }
    toString() {
        console.log("toString");
        return "哈哈哈";
    }
}
let a = new A();

console.log(String(a)); // toString  哈哈哈
console.log(Number(a)); // valueOf   2
console.log(a + "22"); // valueOf   '222'
console.log(a == 2); // valueOf   true
console.log(a === 2); // false     => (严格等于不会触发隐式转换)

# 相关应用:如何让(a===1&&a===2&&a===3)的值为 true?

在双等于==的时候,因为涉及隐形类型转换可以通过改写valueOf来实现。

但在===时,不会通过 valueOf 进行类型转换,因此可以尝试通过 Object.defineProperty 的 get 去进行改写。

//  让a==1&&a==2&&a==3 => true
const A = function(val) {
    this.val = val;
};
A.prototype.valueOf = function() {
    console.log("valueOf", this.val);
    return this.val++;
};

let a = new A(1);
console.log(a == 1 && a == 2 && a == 3);
// valueOf 1
// valueOf 2
// valueOf 3
// true

// 让a===1&&a===2&&a===3 => true
var val = 1;
Object.defineProperty(window, "a", {
    get() {
        console.log("get value", this.val);
        return this.val++;
    },
});

console.log(a === 1 && a === 2 && a === 3);
//get value 1
// get value 2
// get value 3
// true

# 029:柯里化

柯里化:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

柯里化作用: 参数复用、提前返回 和 延迟执行。

// ES6
function curry(fn, ...args) {
    const len = fn.length;

    return function(...curryArgs) {
        const argArr = [...args, ...currArgs];
        if (argArr.length < len) {
            return curry.call(this, fn, ...argArr);
        } else {
            return fn.apply(this, argArr);
        }
    };
}

// ES5
var curry = function curry(fn, arr) {
    arr = arr || [];

    return function() {
        var args = [].slice.call(arguments);
        var arg = arr.concat(args);

        return arg.length >= fn.length ? fn.apply(this, arg) : curry(fn, arg);
    };
};

# 030:实现一个 add 方法

// 实现一个 add 方法,使计算结果能够满足如下预期:add(1)(2)(3)()=6`,`add(1,2,3)(4)()=10
function add1(...args) {
    let allArgs = [...args];

    const fn = function(...addArgs) {
        // 没有参数时进行求和
        if (addArgs.length === 0) {
            return allArgs.reduce((prev, cur) => prev + cur, 0);
        }
        // 有参数时push进args数组
        allArgs = allArgs.concat(addArgs);
        return fn;
    };

    return fn;
}
// test
console.log(add1(1)(2)(3)());
console.log(add1(1, 2, 3)(4)());


// 在Function需要转换为字符串时,通常会自动调用函数的 toString 方法,因此也可以尝试通过toString方法实现
// 这个方式还兼容支持,add(1)(2)(3)=6, add(1,2,3)(4)=10
function add2(...args) {
    let sum = args.length > 0 ? .reduce((prev, cur) => prev + cur, 0) : 0;

     const fn = function(...addArgs) {
        sum = addArgs.length > 0 ? addArgs.reduce((prev, cur) => prev + cur, sum) : sum;
        return fn;
    };

    fn.toString = function() {
        return sum;
    };

    return fn;
}

// test
console.log(add2(1)(2)(3)()); // f 6
console.log(add2(1, 2, 3)(4)); // f 10
console.log(add2(1, 2, 3)(4)()); // f 10

let val = add2(2)(3);
val = val(4);
console.log(val, val.toString(), typeof val); // f 9 , 9 , funtion


// add3
function add3(...args) {
    let allArgs = [...args];

    const fn = function(...addArgs) {
        allArgs = allArgs.concat(addArgs);
        return fn;
    }

    fn.toString = function() {
        if(allArgs.length === 0) {
            return 0;
        }
        return allArgs.reduce((prev, cur) => prev + cur, 0)
    }

    return fn;
}

# 031:实现 String 的 indexOf 方法

String.prototype.myIndexOf = function(str) {
    let sourceArr = this.split("");
    let num = -1;
    const firstStr = str[0];
    for (let i = 0; i < sourceArr.length; i++) {
        if (sourceArr[i] === firstStr) {
            if (str === this.slice(i, i + str.length)) {
                num = i;
                break;
            }
        }
    }

    return num;
};

# 032:类数组转数组的方法