2021年前端面试javascript专区

一、javascript基础知识

1.1 基本类型和数据类型

基本类型:null、undefinded、number、string、boolean、symbol;

引用类型:Object、Array、Function、Date、RegExp;

除去上面的基本类型,其他的都是对象。基本类型没有附加方法; 所以你永远不会看到undefined.toString()。 也正因为如此,基本类型是不可变的,因为它们没有附加的方法可以改变它:

let s = "boy";
s.bar = "girl";
console.log(s.bar); // undefined

而默认情况下,对象是可变的,可以添加方法:

let obj = {};
obj.foo = 123;  
console.log(obj.foo); // 123

与作为引用存储的对象不同,原始类型作为值本身存储。 这在执行相等性检查时会产生影响:

"dog" === "dog"; // true
14 === 14; // true
 
{} === {}; // false
[] === []; // false
(function () {}) === (function () {}); // false

三个基本类型string,number和boolean,它们有时被当做包装器类型,并且在原始值和包装类型之间进行转换很简单:

  • 原始类型 to 包装类型: new String("abc");
  • 包装类型 to 原始类型: new String("abc").valueOf();

比如字符串“abc”之类的原始值与new String(“abc”)之类的包装器实例有根本上的不同。 例如(用typeof和instanceof判断时):

typeof "pet";  //"string"
typeof new String("pet");  //"object"
    
"pet" instanceof String;  // false
new String("pet") instanceof String;  // true
    
"pet" === new String("pet");  // false

其实包装器实例就是一个对象,没办法在JavaScript中比较对象,甚至不能通过非严格相等 ==:

var a = new String("pet");
var b = new String("pet");
a == b;  // false
a == a;  // true

基本类型

  • 基本类型的变量是存放在栈内存(Stack)里的;
  • 基本数据类型的值是按值访问的;
  • 基本类型的值是不可变的;
  • 基本类型的比较是它们的值的比较;

引用类型

  • 引用类型的值是保存在堆内存(Heap)中的对象(Object);
  • 引用类型的值是按引用访问的;
  • 引用类型的值是可变的;
  • 引用类型的比较是引用的比较;

栈内存是自动分配内存的。而堆内存是动态分配(new的过程)内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为null(js垃圾回收),从而减少无用内存的消耗。

前端面试:谈谈 JS 垃圾回收机制

1.2 0.1+0.2>0.3

js中浮点数使用64位固定长度来表示,具体是按照IEEE754 这个标准来的,这个标准权衡了精度和表示范围,也就是如何有效利用这64个二进制数字的前提下提出的。下面的所有流程都是按这个标准来的,其中把64位划分出了3个区域:

  • 区域 S 符号位 用 1 位表示 0表示正数 1表示负数;
  • 区域 E 指数位 用 11 位表示 有正负范围,临界值是1023 后面看转换过程就能看明白;
  • 区域 M 尾数位 用 52 位表示;

提示:

  • 十进制的小数转换为二进制,主要是小数部分乘以2,取整数部分依次从左往右放在小数点后,直至小数点后为0。例如0.1,要转换为二进制的小数。
  • 二进制的小数转换为十进制主要是乘以2的负次方,从小数点后开始,依次乘以2的负一次方,2的负二次方,2的负三次方等。

1). 而0.1转为二进制是一个无限循环数0.00011001100110011001100110011001100110011001100110011001100110011001......(1100循环)

$0.1*2=0.2$ 取0

$0.2*2=0.4$ 取0

$0.4*2=0.8$ 取0

$0.8*2=1.6$ 取1

$0.6*2=1.2$ 取1

$0.2*2=1.4$ 取0

...

2). 转换为指数格式其实就是移动小数点,让小数点前面出现的是第一个为1的值,不同的二进制数据,可能是前移可能是右移,对应的是指数的正负范围,转换后是这样子的:

1.1001100110011001100110011001100110011001100110011001100110011001.....

这里可以看到向右移动了4位,这个数据会保存在指数区域E内,在没有移位的情况下指数区域的值是1023,向左移动几位就加几位,向右移动几位就减几位,所以这里是:

1023 - 4 = 1019
1019 转二进制并补齐11位  01111111011
复制代码

也就是E为 01111111011,由于尾数位最多只有52位,所以小数点后面的52位全部提取到尾数位,其中要注意的是,类似四舍五入,如果末位后是1会产生进位,这里就产生了进位:

1001100110011001100110011001100110011001100110011001100110011001.....
1001100110011001100110011001100110011001100110011001 100110011001.....
进位后截取
1001100110011001100110011001100110011001100110011010

也就是M为 1001100110011001100110011001100110011001100110011010

这里由于丢掉了部分数据,所以导致精度丢失

3). 由于0.1是正数,所以 S 为 0。

接下来,我们看看 0.1 在内存中是长什么样子的:

let bytes = new Float64Array(1);// 64位浮点数
bytes[0] = 0.1;// 填充0.1进去
let view = new DataView(bytes.buffer);
console.log(view.getUint8(0).toString(2));// 10011010
console.log(view.getUint8(1).toString(2));// 10011001
console.log(view.getUint8(2).toString(2));// 10011001
console.log(view.getUint8(3).toString(2));// 10011001
console.log(view.getUint8(4).toString(2));// 10011001
console.log(view.getUint8(5).toString(2));// 10011001
console.log(view.getUint8(6).toString(2));// 10111001
console.log(view.getUint8(7).toString(2));// 00111111 这里补齐了8位

这里的bytes.buffer代表的就是一串内存空间,为了方便大家理解,这里使用 DataView用无符号8位的格式一个一个地读取内存的数据再转为二进制格式。 由于读取内存的顺序会受字节序的影响,可能在你们的电脑打印得到相反的顺序 如果按SEM的排列,那么其二进制就像下面这样子的:

s(0)E(01111111011)M(1001100110011001100110011001100110011001100110011010)。

把它存到内存中再取出来转换成十进制就不是原来的0.1了,就变成了0.100000000000000005551115123126:

1). 提取尾数位数据:1001100110011001100110011001100110011001100110011010;

2). 先前添加 1, 恢复为指数格式,并提取指数位:

1.1001100110011001100110011001100110011001100110011010

01111111011 => 1019
1019 - 1023 = -4

3) 移位:

0.00011001100110011001100110011001100110011001100110011010

4). 二进制转化为十进制

0.00011001100110011001100110011001100110011001100110011010 =>
0.100000000000000005551115123126

02+0.1:

// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
 
// 转成十进制正好是 0.30000000000000004

那既然0.1不是0.1了,为什么在console.log(0.1)的时候还是0.1呢?

在console.log的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串。

js浮点数存储精度丢失原理

1.3 0.2+0.3=0.5

// 0.2 和 0.3 都转化为二进制后再进行计算
0.001100110011001100110011001100110011001100110011001101 +
0.0100110011001100110011001100110011001100110011001101 = 
0.10000000000000000000000000000000000000000000000000001 
 
// 而实际取值只取52位尾数位,就变成了
0.1000000000000000000000000000000000000000000000000000   //0.5

0.2 和0.3分别转换为二进制进行计算:在内存中,它们的尾数位都是等于52位的,而他们相加必定大于52位,而他们相加又恰巧前52位尾数都是0,截取后恰好是0.1000000000000000000000000000000000000000000000000000也就是0.5

1.4 判断数据类型

1). typeof:返回数据类型,包含这7种: number、boolean、symbol、string、object、undefined、function。

  • typeof null   返回类型错误,返回object;
  • 针对引用类型,除了function返回function类型外,其他均返回object;

其中,null 有属于自己的数据类型 Null , 引用类型中的数组、日期、正则 也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,没有错,但不是我们想要的结果。

function getName() {
      console.log("xxxx");
}

typeof undefined // 'undefined' 
typeof '10' // 'string' 
typeof 10 // 'number' 
typeof false // 'boolean' 
typeof Symbol() // 'symbol' 
typeof Function // ‘function' 
typeof null // ‘object’ 
typeof [] // 'object' 
typeof {} // 'object'
typeof new getName()  //作为构造函数创建 对象实例 类型object
typeof getName  //function

为什么typeof null是Object:

因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object

这个bug是初版本的JavaScript中留下的,扩展一下其他五种标识位:

  • 000 对象
  • 1 整型
  • 010 双精度类型
  • 100字符串
  • 110布尔类型

2). toString :toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString()  就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

判断类型举例:

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

3). constructor:constructor是原型prototype的一个属性,当函数被定义时候,js引擎会为函数添加原型prototype,并且这个prototype中constructor属性指向函数引用, 因此重写prototype会丢失原来的constructor。

var d = new Number(1)
var e = 1
function fn() {
  console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;

console.log(e.constructor);//ƒ Number() { [native code] }
console.log(e.constructor.name);//Number
console.log(fn.constructor.name) // Function 
console.log(date.constructor.name)// Date 
console.log(arr.constructor.name) // Array 
console.log(reg.constructor.name) // RegExp

null 和 undefined 无constructor,这种方法判断不了。

如果自定义对象,开发者重写prototype之后,原有的constructor会丢失,因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。

4). instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,只能判断对象是否存在于目标类型的原型链上:

 

由上图可以看出[]的原型(__proto__)指向Array.prototype,间接指向Object.prototype, 因此 [] instanceof Array 返回true, [] instanceof Object 也返回true。

function Foo() { };
var f1 = new Foo();
var d = new Number(1);

console.log(f1 instanceof Foo);// true
console.log(d instanceof Number); //true
console.log(123 instanceof Number); //false   -->不能判断字面量的基本数据类型

自己实现:

function myInstance(L, R) {//L代表instanceof左边,R代表右边
    var RP = R.prototype
    var LP = L.__proto__
    while (true) {
        if (LP == null) {
            return false
        }
        if (LP == RP) {
            return true
        }
        LP = LP.__proto__
    }
}
console.log(myInstance({}, Object)); 

 js的原型和原型链

「每日一题」什么是 JS 原型链?

1.5 == 和 ===

===是严格意义上的相等,会比较两边的数据类型和值大小:

  • 数据类型不同返回false;
  • 数据类型相同,但值大小不同,返回false;

==是非严格意义上的相等:

  • 两边类型相同,比较大小;

  • 两边类型不同,根据下方表格,再进一步进行比较;

    • Null == Undefined ->true;
    • String == Number ->先将String转为Number,再比较大小;
    • Boolean == Number ->现将Boolean转为Number,再进行比较;
    • Object == String,Number,Symbol -> Object 转化为原始类型;

1.6 call、apply、bind区别

通过call、apply以及bind方法改变this的行为,其中call与apply比较特殊,它们在修改this的同时还会直接执行方法,而bind只是返回一个修改完this的boundFunction并未执行.

let obj1 = {
    name: '听风是风'
};
let obj2 = {
    name: '时间跳跃'
};
let obj3 = {
    name: 'echo'
}
var name = '行星飞行';

function fn() {
    console.log(this.name);
};
fn(); //行星飞行   由于函数调用时前面并未指定任何对象,这种情况下this指向全局对象window。
fn.call(obj1); //听风是风
fn.apply(obj2); //时间跳跃
fn.bind(obj3)(); //echo

在js中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而call与apply让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用。

除了都能改变this指向并执行函数,call与apply唯一区别在于参数不同,具体如下:

var fn = function (arg1, arg2) {
    // do something
};

fn.call(this, arg1, arg2); // 参数散列
fn.apply(this, [arg1, arg2]) // 参数使用数组包裹

call第一参数为this指向,后续散列参数均为函数调用所需形参,而在apply中这些参数被包裹在一个数组中。

call实用案例:

function type(obj) {
    var regexp = /s(w+)]/;
    var result =  regexp.exec(Object.prototype.toString.call(obj))[1];
    return result;
};

console.log(type([123]));//Array
console.log(type('123'));//String
console.log(type(123));//Number
console.log(type(null));//Null
console.log(type(undefined));//Undefined

注意,如果在使用call之类的方法改变this指向时,指向参数提供的是null或者undefined,那么 this 将指向全局对象。

1.7 实现一个call

我们从一个简单的例子解析call方法:

var name = '时间跳跃';     // 不可以使用let、var全局变量
let obj = {
    name: '听风是风'
};

function fn() {
    console.log(this.name);
};
fn(); //时间跳跃
fn.call(obj); //听风是风

在这个例子中,call方法主要做了两件事:

  • 修改了this指向,指向obj,比如fn()默认指向window,所以输出时间跳跃;
  • 执行了函数fn;

先说第一步改变this怎么实现,其实很简单,只要将方法fn添加成对象obj的属性不就好了。所以我们可以这样:

Function.prototype.myCall = function (obj) {
    // 不传递默认为window
    obj = obj || window;

    // 此时this就是函数fn,即myCall的调用者
    obj.fn = this;

    // 保存参数
    let args = Array.from(arguments).slice(1)   //Array.from 把伪数组对象转为数组

    // 调用函数
    let result = obj.fn(...args);

    delete obj.fn

    return result
}

其中arguments为函数默认属性,代指函数接收的所有参数,它是一个伪数组。

测试代码:

fn.myCall(obj, "我的", "名字", "是"); // 我的名字是听风是风
fn.myCall(null, "我的", "名字", "是"); // 我的名字是时间跳跃
fn.myCall(undefined, "我的", "名字", "是"); // 我的名字是时间跳跃    

1.8.实现一个apply方法

apply方法因为接受的参数是一个数组,所以模拟起来就更简单了,理解了call实现,我们就直接上代码:

Function.prototype.myApply = function (obj) {
    // 不传递默认为window
    obj = obj || window;

    // 此时this就是函数fn,即myCall的调用者
    obj.fn = this;

    // 是否传参
    if (arguments[1]) {
        result = obj.fn(...arguments[1])
    } else {
        result = obj.fn()
    }

    delete obj.fn

    return result
}

测试代码:

fn.myApply(obj, ["我的", "名字", "是"]); // 我的名字是听风是风
fn.myApply(null, ["我的", "名字", "是"]); // 我的名字是时间跳跃
fn.myApply(undefined, ["我的", "名字", "是"]); // 我的名字是时间跳跃

js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解

js 实现call和apply方法,超详细思路分析

1.9 实现一个bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

说的通俗一点,bind与apply/call一样都能改变函数this指向,但bind并不会立即执行函数,而是返回一个绑定了this的新函数,你需要再次调用此函数才能达到最终执行。

此外:返回的bind函数除了支持不普通调用,还支持new调用,这两者this指向不同,new的this指向创建的实例对象,直接调用this指向我们绑定的对象。

我们知道(强行让你们知道)构造函数实例的constructor属性永远指向构造函数本身,比如:

function Fn(){};
var o = new Fn();
console.log(o.constructor === Fn);//true

而构造函数在运行时,函数内部this指向实例,所以this的constructor也指向构造函数:

function Fn() {
    console.log(this.constructor === Fn); //true
};
var o = new Fn();
console.log(o.constructor === Fn); //true

所以我就用constructor属性来判断当前bound方法调用方式,毕竟只要是new调用,this.constructor === Fn一定为true。

Function.prototype.myBind = function (obj) {
    // 判断调用者是不是函数
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    };

    //第0位是this,所以得从第一位开始裁剪
    let args = Array.prototype.slice.call(arguments, 1);
    let fn = this;

    let bound = function () {
        //二次调用我们也抓取arguments对象
        let params = Array.prototype.slice.call(arguments);
        //通过constructor判断调用方式,为true this指向实例,否则为obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
    };

    //原型链继承
    bound.prototype = fn.prototype;
    return bound;
}

测试代码:

var z = 0;
let obj = {
    z: 1
};

function fn(x, y) {
    this.name = '听风是风';
    console.log(this.z);
    console.log(x);
    console.log(y);
};
fn.prototype.age = 26;

let bound = fn.myBind(obj, 2);
bound(3);                  // this指向Obj  1 2 3 
let person = new bound(3); //undefined 2 3 new的this指向创建的实例对象
console.log(person.name); //听风是风
console.log(person.age); //26

person.__proto__.age = 18;   // 修改原型
person = new fn();      // new的this指向创建的实例对象 undefined undefined undefined
console.log(person.age); //18.

js 手动实现bind方法,超详细思路分析!

1.10 js创建对象的方式

  • 使用内置对象;
  • 使用json符号;
  • 自定义对象构造;

1) 使用内置对象

js可用的内置对象分为两种:

  • js语言原生对象:Function、Object、Date等;
  • JavaScript运行期的宿主对象(环境宿主级对象),如window、document、body等。

我们所说的使用内置对象,是指通过JavaScript语言原生对象的构造方法,实例化出一个新的对象。如:

let str = new String("实例初始化String");
let func = new Function("x","alert(x)");//示例初始化func
let o = new Object();//示例初始化一个Object
console.log(str instanceof Object); // true
console.log(func instanceof Object); // true
console.log(o instanceof Object); // true

2) 使用json符号

在JavaScript中,json被理解为对象。通过字符串形式的json,数据可以很方便地解析成JavaScript独享,并进行数据的读取传递。

例如:

let json = {
    book:[
        {name:"三国演义"},
        {name:"西游记"},
        {name:"水浒传"},
        {name:"红楼梦"}
        ],
    author:[
        {name:"罗贯中"},
        {name:"吴承恩"},
        {name:"施耐安"},
        {name:"曹雪芹"}
        ],
    sayHello:()=>{
        console.log('Hello world');
    }
};

json.sayHello();

3) 自定义对象构造

创建高级对象构造有两种方式:使用“this”关键字构造、使用原型prototype构造。如:

//使用this关键字定义构造的上下文属性
function Girl() {
    this.name = "big pig";
    this.age = 20;
    this.standing;
    this.bust;
    this.waist;
    this.hip;
}

//使用prototype
function Girl() { }
Girl.prototype.name = "big pig";
Girl.prototype.age = 20;
Girl.prototype.standing;
Girl.prototype.bust;
Girl.prototype.waist;
Girl.prototype.hip;
alert(new Girl().name);

上例中的两种定义在本质上没有区别,都是定义“Girl”对象的属性信息。“this”与“prototype”的区别主要在于属性访问的顺序。如:

function Test() {
    this.test = function () {
        alert("defined by this");
    }
}
Test.prototype.test = function () {
    alert("defined by prototype");
}
var _o = new Test();
_o.test();//输出“defined by this”

当访问对象的属性或者方法是,将按照搜索原型链prototype chain的规则进行。首先查找自身的静态属性、方法,继而查找构造上下文的可访问属性、方法,最后查找构造的原型链。

“this”与“prototype”定义的另一个不同点是属性的占用空间不同:

  • 使用“this”关键字,实例初始化时为每个实例开辟构造方法所包含的所有属性、方法所需的空间;
  • 而使用“prototype”定义,由于“prototype”实际上是指向父级的一种引用,仅仅是个数据的副本,因此在初始化及存储上都比“this”节约资源。

1.11 函数和对象区别

每一个javascript函数都表示为一个对象,更确切的数,是Function对象的一个实例。

let Parent = function(name,age){
    this.name = name;
    this.age = age;
}

console.log(Parent instanceof Object);
console.log(Parent instanceof Function);
console.log(typeof  Parent);

JavaScript中函数和对象的区别

function Test (word) {
    console.log (word);
    console.log(this.constructor === Test);
}
 
Test('我是函数');  //我是函数  false
 
new Test('我是对象');   // 我是对象 true
 
 
//将以上的调用方式换种通俗易懂的方式
 
Test.call();     //相当于Test(); undefined false
 
//相当于new Test();
let obj = {};
obj._proto_ = Test.prototype;
Test.call(obj);   // undefined false(this为obj)

函数直接调用和new 实例对象,两次调用之中的this不同。调用Test('...');的时候,里面的this是顶级对象window,返回值是undefined。调用new Test('...');的时候,它会先new一个对象,置类型为Test,之后把它作为this执行Test函数,最后再把对象返回。

1.12  字面量创建对象和new创建对象有什么区别

我们先来说以下字面量,也就是使用json创建对象,这种方式创建对象:

  • 字面量创建对象简单,方便阅读;
  • 不需要作用域解析,速度更快(没有this);

我们再来说一下new,下面的例子中分别通过构造函数与class类实现了一个简单的创建实例对象的过程。

// ES5构造方法
let Parent = function(name,age){
    this.name = name;
    this.age = age;
}

// 给原型添加方法
Parent.prototype.sayHello = function(){
    console.log(this.name);
}

let child = new Parent('听风是雨',26);
child.sayHello();

//ES6 class类
class CParent{
    // 构造方法
    constructor(name,age){
        this.name = name;
        this.age = age;
    }

    sayHello(){
        console.log(this.name);
    }
}

let cChild = new CParent('听风是雨',26);
cChild.sayHello();

那么new到底做了什么?我们如何实现一个new。

比较直观的感觉,当我们new一个构造函数,得到的实例继承了构造器的构造属性(this.name这些)以及原型上的属性。

new的过程说的比较直白,当我们new一个构造器,主要有三步:

  • 创建一个新对象;
  • 使新对象的__proto__指向原函数的prototype;
  • 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result;
  • 判断执行函数的结果是不是null或undefined,如果是则返回之前的新对象,如果不是则返回result;
function myNew(fn, ...args){
    let obj = {};
    // 指向fn原型
    obj.__proto__ = fn.prototype;
    // fn内部this指向obj
    let result = fn.apply(obj, args)
    // 返回
    return result instanceof Object ? result : obj;
}

// ES5构造方法
let Parent = function(name,age){
    this.name = name;
    this.age = age;
}

// 给原型添加方法
Parent.prototype.sayHello = function(){
    console.log(this.name);
}


let child = myNew(Parent,'听风是雨',26);
child.sayHello();

1.13 字面量new出来的对象和 Object.create(null)创建出来的对象有什么区别

字面量和new创建出来的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型;

而 Object.create(null)创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性;

二、javascript作用域

作用域概念是理解javascrip的关键所在,不仅仅从西能角度,还包括从功能的角度。作用域对javascript有许多影响,从确定哪些变量可以被函数访问,到确定this的赋值。

2.1 作用域

通俗的讲:变量和函数的可使用范围称作作用域。在Javascript 中,作用域分为全局作用域 和 函数作用域。

  • 全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。
  • 函数作用域:在固定的代码片段才能被访问。

 

作用域有上下级关系,上下级关系的确定就看函数是在哪个作用域下创建的。如上,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

变量取值:到创建 这个变量的函数的作用域中取值。

2.2 作用域链

一般情况下,变量取值到创建这个变量的函数的作用域中取值。

但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

2.3 执行上下文

执行上下文分为:

  • 全局执行上下文:
    • 创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出;
  • 函数执行上下文:eval执行上下文;
    • 每次函数调用时,都会新创建一个函数执行上下文;
    • 执行上下文分为创建阶段和执行阶段;
      • 创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域;
      • 执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象;

2.4 执行栈

  • 首先栈特点:先进后出;
  • 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行出栈;
  • 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文;
  • 只有浏览器关闭的时候全局执行上下文才会弹出;

2.5 闭包

闭包是javascript最强大的特性之一,它允许函数访问局部作用域之外的数据。函数和函数内部能访问到的变量的总和,就是一个闭包。

闭包常用来间接访问一个变量,换句话说,用来隐藏一个变量。类似强类型语言中的private,这样可以使私有变量不受外界干扰,起到保护作用。

下面演示一个demo:

function foo(){
  let number = 1
  function increment(){  // 访问局部作用域之外的数据number
    number++
    return number
  }
  return increment;
}

let increment = foo();
console.log(increment());    //2
console.log(increment());    //3

上面例子number和increment函数组成了一个闭包。这里将number放到一个函数里,可以起到隐藏变量的作用。

应用:

  • 设计模式中的单例模式;
  • for循环中的保留i的操作;
  • 防抖和节流;
  • 函数柯里化;

「每日一题」JS 中的闭包是什么?

三 javascript原型

 3.1 原型

原型分为隐式原型和显式原型,每个实例对象都有一个隐式原型,它指向自己的构造函数的显式原型。

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

// 创建实例对象
let person= new Person("ce");

// 变量实例对象隐式原型  === 构造函数的显式原型
console.log(person.__proto__ === Person.prototype);   //true
console.log(Person === Person.prototype.constructor); //true

js函数提供了一个属性prototype,函数的这个原型指向一个对象,这个对象叫原型对象。这个原型对象有一个constructor属性,指向这个函数本身。

js实例对象提供了一个属性__proto__,一般也被称作隐身原型,它指向自己的构造函数的显式原型。

文字描述往往没有图像描述来的直观,那么我们用一个三者之间的关系图来直观的了解。

原型作用:

  • 原型作用之一:多个实例对象共享一个原型对象,节省内存空间;
  • 原型作用之二:实现继承;

什么是Js原型?(1)(包括作用:继承)

3.2 原型实现继承

比如,现在有一个"动物"对象的构造函数。

function Animal(){
    this.specis = "动物";
}

还有一个"猫"对象的构造函数。

function Cat(name,color){
    this.anme = name;
    this.color = color;
}

怎样才能使"猫"继承"动物"呢? 如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。

Cat.prototype = new Animal();  // 原型对象  子类共享父类所有属性,不能传参
Cat.prototype.constructor = Cat;
let cat = new Cat("大猫","yellow");
console.log(cat.specis);

3.3 原型链

多个__proto__组成的继承关系成为原型链。

  • 所有实例对象的__proto__都指向他们构造函数的prototype;
  • 所有构造函数prototype都是对象,自然它的__proto__指向的是Object;
  • 所有的构造函数的隐式原型指向的都是Function的显示原型;
  • Object的隐式原型是null;

四、javascript中级知识

4.1 js中的常用的继承方式有哪些?以及各个继承方式的优缺点

1). 原型链继承:
function Animal(){
    this.specis = "动物";
}

function Cat(name,color){
    this.anme = name;
    this.color = color;
}

Cat.prototype = new Animal();  // 修改原型对象  子类共享父类所有属性,不能传参
Cat.prototype.constructor = Cat;
let cat = new Cat("大猫","yellow");
console.log(cat.specis);   //动物
console.log(JSON.stringify(cat));  //{"anme":"大猫","color":"yellow"}
通过修改原型对象实现。子类共享父类所有属性,并且属性只读,无法修改,但是可以通过为子类实例对象添加新的同名属性覆盖父类中的属性。

2). 构造函数继承:

function Parent(name,age){
    this.name = name;
    this.age = age;
}

Parent.prototype.sex = '男';


function Child(phone,name,age){
    // 调用父类构造函数
    Parent.call(this,name,age);
    this.phone = phone;
}

let ins = new Child('181','zy','26');
console.log(JSON.stringify(ins));  //{"name":"zy","age":"26","phone":"181"}

可以看到构造函数继承只会继承父类构造函数中的属性和方法,而不会继承原型中的属性和方法,无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

这种模式解决了两个问题,就是可以传参,可以继承,但是没有原型,就没有办法复用。

3) 组合方式继承(推荐):

function Parent(name,age){
    this.name = name;
    this.age = age;
}

Parent.prototype.sex = '男';


function Child(phone,name,age){
    // 调用父类构造函数
    Parent.call(this,name,age);
    this.phone = phone;
}

// 更改原型对象
Child.prototype = new Parent();   

console.log(Child.prototype.constructor === Parent);//true
// 修改原型对象构造
Child.prototype.constructor = Child;

let ins = new Child('181','zy','26');
ins.sex= '女';
console.log(JSON.stringify(ins));   //  {"name":"zy","age":"26","phone":"181","sex":"女"}
console.log(ins.sex);   //

let ins1 = new Child('181','zy','26');
console.log(ins1.sex);   //

这种继承方式的思路是:用使用原型链的方式来实现对原型属性和方法的继承,而通过借用构造函数来实现对构造函数中属性和方法的继承。

此时我们是无法修改父原型中的属性的,以sex为例,我们设置ins.sex实际是给当前实例新增了一个属性sex,而不是修改原型链上的sex属性。

但是,修改引用属性如对象则会追根溯源到原型链上,修改后就会影响后面创建的实例对象。

因此如果有代码想要重复使用而不是要反复修改的属性,就写在原型上去继承,而表示特色的属性尤其是要经常修改的普通对象要写在构造函数上。

4) es6方式继承:

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。

class Person {
    //调用类的构造方法
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    //定义一般的方法
    showName() {
        console.log("调用父类的方法")
        console.log(this.name, this.age);
    }
}
let p1 = new Person('kobe', 39);
console.log(p1);   //{"name":"kobe","age":39}

//定义一个子类
class Student extends Person {
    constructor(name, age, salary) {
        super(name, age)//通过super调用父类的构造方法必须
        this.salary = salary
    }
    showName() {//在子类自身定义方法
        console.log("调用子类的方法")
        console.log(this.name, this.age, this.salary);
    }
}

let s1 = new Student('wade', 38, 1000000000);  
console.log(s1);   //{"name":"wade","age":38,"salary":1000000000}
s1.showName();

相对ES5继承来书,ES6继承语法简单易懂,操作更方便。

JS中常见的几种继承方法

JavaScript常见的六种继承方式

4.2 内存泄漏

​内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏。

常见的内存泄漏:

1) 意外的全局变量引起的内存泄漏:全局变量,不会被回收。解决:使用严格模式

2) 被遗忘的定时器或者回调:定时器中有dom的引用,即使dom删除了,但是定时器还在,所以内存中还是有这个dom;解决:手动删除定时器和dom。

3) 循环引用

function assignHandler(){
    let element = document.getElementById('someElement');
    // 匿名函数引用了element
    element.onclick = function(){
        alert(element.id);
    }
}

以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用,匿名函数中保存了一个对 element 对象(存在堆内存中)的引用,因此无法减少 element 的引用数。只要匿名函数在,element 的引用数至少是 1,因此它所占用的内存就永远无法回收。

解决办法:

function assignHandler(){
    let element = document.getElementById('someElement');
    let id = element.id;
    // 匿名函数引用了element
    element.onclick = function(){
        alert(id);
    }
    // 解除对dom的引用 引用次数变成了0,内存就可以释放出来了。
    id=null;
    element = null;
}

js常见的内存泄漏

4.3 为什么会有内存泄漏

内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃。

4.4 垃圾回收机制都有哪些策略

  • 标记清除法:当变量进入执行环境事,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
  • 引用计数:含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

JavaScript垃圾回收机制

4.5 深拷贝和浅拷贝

如何区分深拷贝与浅拷贝,简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短,如果B没变,那就是深拷贝,自食其力。

对于引用数据类型,名字是存储在栈内存中,值存储在堆内存中,但栈内存保存堆内存中的引用地址;

比如浅拷贝:

 当b=a进行拷贝时,其实复制的是a的引用地址,而并非堆里面的值。

 而当我们a[0]=1时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。

那,要是在堆内存中也开辟一个新的内存专门为b存放值,那就是深拷贝。

浅拷贝的实现:

1) 遍历对象,引用赋值

function simpleCopy(obj1) {
   var obj2 = Array.isArray(obj1) ? [] : {};
   for (let i in obj1) {
   obj2[i] = obj1[i];
  }
   return obj2;
}

2) Object.assign:方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

const object1 = {
  a: 1,
  b: 2,
  c: 3,
  ref: {
      a:1,
      b:2
  }
};
const object2 = Object.assign({c: 4, d: 5}, object1);
object1.ref.a = 11111111111111;
console.log(object1);  // {"a":1,"b":2,"c":3,"ref":{"a":11111111111111,"b":2}}
console.log(object2);  //  {"a":1,"b":2,"c":3,"ref":{"a":11111111111111,"b":2}}

可以看出ref在拷贝的时候仅仅是引用赋值。

深拷贝的实现:

1) JSON对象实现

function Person(name,age){
    this.name = name;
    this.age = age;
    this.ref = {
        a:1,
        b:2
    }

    function sayHello(){
        console.log(this.name);
    }
}


function deepClone(obj){
    return JSON.parse(JSON.stringify(obj));
}

let ins = new Person('zy',26);
let insCopy = deepClone(ins);
insCopy.ref.a = '11';
console.log(ins);    // {"name":"zy","age":26,"ref":{"a":1,"b":2}}
console.log(insCopy);   //{"name":"zy","age":26,"ref":{"a":"11","b":2}}

2) 采用递归去拷贝所有层级属性

function Person(name,age){
    this.name = name;
    this.age = age;
    this.ref = {
        a:1,
        b:2
    }

    function sayHello(){
        console.log(this.name);
    }
}


function deepClone(obj){
    // 如果是null  typeof null === object
    if(obj === null){
        return null;
    }

    // 基本类型
    if(typeof obj !== 'object'){
        return obj;
    }

    let objClone = Array.isArray(obj) ? [] : {};
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            objClone[key] = deepClone(obj[key]);
        }
    }
    
    return objClone;
}

let ins = new Person('zy',26);
let insCopy = deepClone(ins);
insCopy.ref.a = '11';
console.log(ins);    // {"name":"zy","age":26,"ref":{"a":1,"b":2}}
console.log(insCopy);   //{"name":"zy","age":26,"ref":{"a":"11","b":2}}
console.log(deepClone(1));  // 1
console.log(deepClone("1"));  // 1
console.log(deepClone(false));  // false
console.log(deepClone(null));  // null
console.log(deepClone(undefined));  // undefined
console.log(deepClone());  //undefined
console.log(deepClone([1,2,3,4]));  //[1,2,3,4]

3) lodash库函数

let result = _.cloneDeep(test)

从上面例子可以看出,深拷贝出来的对象和原对象之间没有任何数据是共享的,所有的东西都是自己独占的一份。

js浅拷贝与深拷贝的区别和实现方式

五、javascript高级知识

5.1 为什么js是单线程的

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

js中任务分为两种,同步任务和异步任务:

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;比如下面的例子,只有f1()任务执行完后,才会执行f2()任务。

function f1(){
    console.log(1);
}

function f2(){
    console.log(2);
}

f1();   //1
f2();   //2

异步任务:不进入主线程、而进入”任务队列”的任务,当主线程中的任务执行完,才会从任务队列中取出异步任务放入主线程执行。

既然js将任务分为同步任务和异步任务,那哪些任务是异步任务?一般而言,异步任务有三种:

  1. 普通事件,click、resize事件等;
  2. 资源加载,如load,error等;
  3. 定时器,setTimeout,setInterval等;

js的执行机制(事件循环,这个过程会不断重复):

  • 首先判断js代码是同步还是异步,同步就进入主线程,异步就进入event table;
  • 异步任务在event table中注册函数,当满足触发条件后,被推入event queue;
  • 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有,就推入主线程中执行;

为什么javascript是单线程?

5.2 如何实现异步编程

回调函数。所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

5.3 es6 generator发生器

语法:

function* myGenerator() {
  yield 'hello';
  yield 'world';
  return 'Generator';
}

let g = myGenerator();

console.log(g.next()); // { value: 'hello', done: false }
console.log(g.next()); // { value: 'world', done: false }
console.log(g.next()); // { value: 'ending', done: true }
console.log(g.next()); // { value: undefined, done: true }

分析Generator的执行过程:

  • 第一次调用,Generator函数开始执行,直到遇到第一个yield表达式为止;
  • 第二次调用,Generator函数从上次yield表达式停下的地方,一直执行到下一个yield表达式;
  • 第n-1次调用,Generator函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束);
  • 第n次调用,Generator函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true;

通过调用迭代器的next方法来请求一个一个的值,返回的对象有两个属性,一个是value,也就是值;另一个是done,是个布尔类型,done为true说明生成器函数执行完毕,没有可返回的值了。

状态变化:

  • 每当执行到yield属性的时候,都会返回一个对象;
  • 这时候生成器处于一个非阻塞的挂起状态;
  • 调用迭代器的next方法的时候,生成器又从挂起状态改为执行状态,继续上一次的执行位置执行;
  • 直到遇到下一次yield依次循环;
  • 直到代码没有yield了,就会返回一个结果对象done为true,value为undefined;

5.4 说说 Promise 的原理?你是如何理解 Promise 的

Promise是最早由社区提出和实现的一种解决异步编程的方案,比其他传统的解决方案(回调函数和事件)更合理和更强大。

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

1、Promise对象的状态不受外界影响

1)pending 初始状态;

2)fulfilled 成功状态;

3)rejected 失败状态;

Promise 有以上三种状态,只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态

2、Promise的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可以逆,只能由 pending变成fulfilled或者由pending变成rejected;

使用案例:
const promise = new Promise(function (resolve, reject) {
    // ... some code
    let value = Math.random() * 10;

    // 异步操作成功
    if (value > 5) {
        resolve(value);
    } else {
        reject(new Error('值大于2'));
    }
});
  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(value => {
    console.log('成功', value);
}, err => {
    console.log('失败', err);
})
then方法可以接受两个回调函数作为参数。
  • 第一个回调函数是Promise对象的状态变为resolved时调用;
  • 第二个回调函数是Promise对象的状态变为rejected时调用;

其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

手写Promise:

// promise接收一个函数 参数为(resolve, reject)
function MyPromise(task) {

    // 缓存this  
    let that = this;

    // 保存状态
    that.status = 'pending';
    // 保存值
    that.value = undefined;

    // 存放成功后要执行的回调函数的序列
    that.onResolvedCallbacks = [];

    // 存放失败后要执行的回调函数的序列
    that.onRjectedCallbacks = [];

    // resolve方法  在task中调用该方法,resolve使用this的话,this指向不是MyPromise实例
    function resolve(value) {
        // 该方法是将Promise由pending变成fulfilled
        if (that.status === 'pending') {
            that.status = 'fulfilled';
            that.value = value;
            that.onResolvedCallbacks.forEach(callback => callback(value));
        }
    };

    // reject方法  在task中调用该方法,resolve使用this的话,this指向不是MyPromise实例
    function reject(error) {
        // 该方法是将Promise由pending变成rejected
        if (that.status === 'pending') {
            that.status = 'rejected';
            that.value = error;
            that.onRjectedCallbacks.forEach(callback => callback(error))
        }
    };


    try {
        // 每一个Promise在new一个实例的时候 接受的函数都是立即执行的
        task(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

// then方法,接收两个函数onFulfilled onRejected,状态是成功态的时候调用onFulfilled 传入成功后的值,
// 失败态的时候执行onRejected,传入失败的原因,pending 状态时将成功和失败后的这两个方法缓存到对应的数组中,
// 当成功或失败后 依次再执行调用
MyPromise.prototype.then = function (onFulfilled, onRejected) {
    // 成功
    if (this.status === 'fulfilled') {
        onFulfilled(this.value);
    }

    // 失败
    if (this.status === 'rejected') {
        onRejected(this.value);
    }

    // 如果异步过程还未执行完毕
    if (this.status == 'pending') {
        this.onResolvedCallbacks.push(onFulfilled);
        this.onRjectedCallbacks.push(onRejected);
    }
}

const promise = new MyPromise(function (resolve, reject) {
    setTimeout(() => {
        // ... some code
        let value = Math.random() * 10;
        // 异步操作成功
        if (value > 5) {
            resolve(value);
        } else {
            reject(new Error('值大于2'));
        }
    }, 1000);
});

promise.then(value => {
    console.log('成功', value);
}, err => {
    console.log('失败', err);
})

手写Promise.all:

// 接受一个数组,数组内都是Promise实例
// 返回值:返回一个Promise实例,这个Promise实例的状态转移取决于参数的Promise实例的状态变化。
// 当参数中所有的实例都处于resolve状态时,返回的Promise实例会变为resolve状态。
// 如果参数中任意一个实例处于reject状态,返回的Promise实例变为reject状态
MyPromise.all = function (promises) {
    let list = [];
    let count = 0;

    return new MyPromise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            // 处理每一个promise执行结果
            promises[i].then(value => {
                list[i] = value;
                count++
                if (count === promises.length) {
                    resolve(list);
                }
            }, err => {
                reject(err);
            });
        }
    })
}

MyPromise.all([promise]).then(values => {
    console.log('成功', values);
}, err => {
    console.log('失败', err);
})

参考文章

[1]前端面试题2021及答案

[2]身为三本的我就是凭借这些前端面试题拿到百度京东offer的,前端面试题2021及答案(部分转载)

原文地址:https://www.cnblogs.com/zyly/p/15367216.html