常见面试题:js篇

1.暂时性死区

https://segmentfault.com/a/1190000008213835

function foo(x = y, y = 1) {
    console.log(y)
}
foo(1) // 这不会有错误
foo(undefined, 1) // 错误 ReferenceError: y is not defined
foo() // 错误 ReferenceError: y is not defined
解答:

传入undefined, 所以x使用默认值y, 进入TDZ产生错误。
foo(1) 不会让x使用y。
在这里插入图片描述在这里插入图片描述

2.连续赋值:https://www.cnblogs.com/dunken/p/4395825.html

var a = {n:1};
var b = a;
a.x = a = {n:2};
console.log(a.x);// --> undefined
console.log(b.x);// --> [object Object]
解答:

在这里插入图片描述

3.闭包

https://blog.csdn.net/dudufine/article/details/62234296

外部函数能够去使用内部函数的变量,通常函数被执行后,它的上下文环境就会被销毁,但由于闭包的存在,内部函数的变量会一直被保存给外部引用,从而导致了内存泄露 ,应用闭包场合主要是为了:设计私有的方法和变量。 不想让外部修改这些属性,因此就可以设计一个闭包来只提供方法获取。
闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。闭包中的变量并不保存中栈内存中,而是保存在堆内存中
四种常见的js内存泄露:
(1)意外的全局变量
(2)被遗忘的计时器或回调函数
(3)脱离DOM的引用
(4)闭包
应用场景:https://blog.csdn.net/qq_21132509/article/details/80694517
setTimeout,回调,

 var data = [];
    for(let i = 0; i < 3; i++){
            data[i] = function(){
                console.log(i);
            };
    }
    data[0]();
    data[1]();
    data[2]();
解答:

let是严格模式,声明的变量只在let命令所在的代码块有效果,也就是只有在for循环中有效,var i,i为全局变量,循环结束,i保存在内存中不会马上销毁,在for循环结束的时候输入i可以看到i=3;而 data[i] = function(){console.log(i);};在for循环中相当于闭包,data[0]、data[1]、data[2] 函数内的变量i实际都是指向同一个地址,所以输出的值都是一样在这里插入图片描述

4.this指向

https://github.com/wuomzfx/blog/blob/master/this.md

var name = 'window';
var person1 = {
    name: 'person1',
    show1: function () {
        console.log(this.name);
    },
    show2: () => console.log(this.name),
    show3: function () { //高阶函数,返回一个函数,调用者是window
        return function () {
        console.log(this.name);
        }
        //类似var func = person1.show3() func()
    },
    show4: function () {
        return () => console.log(this.name);
    }
};
var person2={name:'person2'};
//this总是指向调用该函数的对象,在全局函数中,this指向window
person1.show1();                                //person1,隐式绑定,this指向调用者person1
person1.show1.call(person2);                    //person2,显示绑定,this指向person2,相当于person2.show1()
//this指向在箭头函数中与当时的上下文this绑定,并且不管在哪儿,以什么方式调用都不会被更改。因此show2的所有调用返回的都是window。
//箭头函数指向的是谁调用箭头函数的外层function,箭头函数的this就是指向该对象,如果箭头函数没有外层函数,则指向window
person1.show2();                                //window,箭头函数没有外层函数调用,所以this指向全局作用域
person1.show2.call(person2);                     //window,这句话相当于person2.show2(),同上,箭头函数没有外层函数调用,所以this指向全局作用域

person1.show3()();                               //window
person1.show3().call(person2);                   //person2,通过person2调用了最终的打印方法
person1.show3.call(person2)();                   //window,先通过person2调用了person1的高阶函数,然后再在全局环境中执行了该打印方法

person1.show4()();                               //person1,这里是person1调用了简头函数的外层函数,所以this指向person1
person1.show4().call(person2);                   //person1,用call(person2)也没用,因为是person1先调用箭头函数的外层, 而且箭头函数的this无法改变,除非改变外层函数的调用者,即person1.show4.call(person2)()
person1.show4.call(person2)();                    //person2,相当于person2.show4()() ,和上面的person1.show4()() 一样

5.|| 和 && 的作用

https://www.cnblogs.com/waisonlong/p/5190614.html

a() && b() :如果执行a()后返回true,则执行b()并返回b的值;如果执行a()后返回false,则整个表达式返回a()的值,b()不执行;
a() || b() :如果执行a()后返回true,则整个表达式返回a()的值,b()不执行;如果执行a()后返回false,则执行b()并返回b()的值;
|| 和 && 常用的是:
在这里插入图片描述

var a = 2;
var b = 3;
var c = 0;
console.log(a && b);//3
console.log(c || b);//3

6.Object.prototype.toString.call(),instanceof()和Array.isArray()的区别和优劣

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/23

Object.prototype.toString.call()每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。
instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。
Array.isArray()用来判断对象是否为数组
instanceof 与 isArray:当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes

//instanceof()无法区分数组,对象和函数,都是返回Object
var a = {};
var b = [];
var c = function () {};

//a b c 都是 Object 的实例
console.log(a instanceof Object) //true
console.log(b instanceof Object) //true
console.log(c instanceof Object) //true

//只有 Array 类型的 b 才是 Array 的实例
console.log(a instanceof Array) //false
console.log(b instanceof Array) //true
console.log(c instanceof Array) //false

//只有 Function 类型的 c 才是 Function 的实例
console.log(a instanceof Function) //false
console.log(b instanceof Function) //false
console.log(c instanceof Function) //true

//typeof
let obj = {};
let arr = [];
console.log(typeof obj === 'object'); //true
console.log(typeof arr === 'object'); //true
console.log(typeof null === 'object'); //true

//Object.prototype.toString.call()区分对象,数组,函数
console.log(Object.prototype.toString.call(123)) //[object Number]
console.log(Object.prototype.toString.call('123')) //[object String]
console.log(Object.prototype.toString.call(undefined)) //[object Undefined]
console.log(Object.prototype.toString.call(true)) //[object Boolean]
console.log(Object.prototype.toString.call({})) //[object Object]
console.log(Object.prototype.toString.call([])) //[object Array]
console.log(Object.prototype.toString.call(function () {})) //[object Function]

//Array.isArray() 用于确定传递的值是否是一个 Array。
Array.isArray([1, 2, 3]); // true
Array.isArray({foo: 123}); // false
Array.isArray("foobar"); // false
Array.isArray(undefined); // false
Array.isArray(null); // false
Array.isArray(new Array()); // true
Array.isArray(Array.prototype); // true

//合并数组a,b
var a=[1,2,3];
var b=[4,5,6];
// var c=Array.prototype.slice.call(a,b);   //a [1,2,3]          c [1,2,3]
// var c=[].concat.call(a,b);               //a [1,2,3]          c [1, 2, 3, 4, 5, 6]
// var c=Array.prototype.push.apply(a, b)   //a [1, 2, 3, 4, 5, 6]
// var c=Array.prototype.push.call(a, b)    //a  [1, 2, 3, Array(3)]

7.针对下面这个ul,为每一个li添加一个点击事件,弹出对应的index(尝试用多种方法,涉及闭包、bind和this)

<ul id="text">
    <li>这是第一个li</li>
    <li>这是第二个li</li>
    <li>这是第三个li</li>
</ul>
解答一:bind,将当前匿名函数指向this,将i当参数传入
var init = function () {
    var obj = document.getElementById('text');
    for (var i = 0; i < obj.children.length; i++) {
        obj.children[i].addEventListener('click', function (i) {
            alert(i)
        }.bind(this, i))
    }
}
init();
解答二:闭包
var init = function () {
    var lis = document.querySelectorAll("#text li");
    for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = (function (i) {
            return function () {
                alert(i);
            };
        })(i)
    }
}
init();
解答三:匹配
var init = function () {
    var obj = document.getElementById('text');
    for (var i = 0; i < obj.children.length; i++) {
        obj.children[i].addEventListener('click', function (item) {
            var self = item.target;
            for (var j = 0; j < obj.children.length; j++) {
                if (self == obj.children[j]) {
                    alert(j);
                }
            }
        })
    }
}
init();

8.cookie && token

cookie放哪里,cookie能做的事情和存在的价值?
----后端生成 放在 http的请求头里 一般5k大小 用于身份认证的功能 携带少量的信息 每次请求都会带上
----识别用户身份(当第一次登录到某个站点时,远端服务器就会传过来一个cookie,里面包含一个随机产生的字符序列,称为用户ID,用来唯一标识用户)
cookie和token都存放在header里面,为什么只劫持前者?
----cookie 每次请求都会带上 内容主要包括:名字,值,过期时间,路径和域,cookie是有状态的 被劫持不安全, 可以设置httpOnly 防止 xss
----token可以设置失效时间, 是无状态的 被劫持后危险性要低一些, 在跨端能力上更好
cookie 由服务器生成,存在客户端,放在 http的请求头里 一般5k大小
服务器返回的cookie会存储在response 的Header里面

HTTP response Header:
{
    "Content-Type" = "text/html;charset=UTF-8";
    Date = "Thu, 29 Jun 2017 08:50:18 GMT";
    Server = "Apache-Coyote/1.1";
    "Set-Cookie" = "token=a27c5779-3718-4716-bbfa-6a51407c6d70";
    "Transfer-Encoding" = Identity;
}

request的Header中也有一个字段用来存储cookie字符串

HTTP request Header:
{
    Cookie = token=c66fbbc7-d799-426e-b065-d23226833dda;
    Content-Type = application/x-www-form-urlencoded; charset=utf-8;
    Device-OS = 10.3;
    version = 1.0.57;
    User-Agent = XProduct/1.0.57 (iPhone; iOS 10.3; Scale/2.00);
    Device-Name = x86_64;
    Accept-Language = zh-Hans-US;q=1, en;q=0.9;
}

因为传统的cookie保存的session id,服务器会根据这个session id,确保服务器与客户端对话;这是的cookie是有状态的,意味着验证记录或者会话需要一直在服务端和客户端保持。而token是无状态的,服务器不记录哪些用户登录了或者哪些 JWT 被发布了,只判断token是否有效,通常我们都会给token设置有效时间,来确保不被劫持。所有劫持cookie比劫持token,更有效,毕竟cookie在某种情况下可以一直使用下去,而token不行。

9.手写call

https://github.com/mqyqingfeng/Blog/issues/11

//手写call
// 1.将函数设为对象的属性
// 2.执行该函数
// 3.删除该函数

//es6
Function.prototype.myCall = function (context) {
    // this 参数可以传 null,当为 null 的时候,视为指向 window
    var context = context || window;
    // 获取调用call的函数,用this可以获取
    context.fn = this
    // 从 Arguments 对象中取值
    let args = [...arguments].slice(1)
    let result = context.fn(...args)
    delete context.fn
    return result
}
// Function.prototype.call2 = function (context) {
//     var context = context || window;
//     context.fn = this;
//     var args = [];
//     for (var i = 1, len = arguments.length; i < len; i++) {
//         args.push('arguments[' + i + ']');
//     }
//     var result = eval('context.fn(' + args + ')');
//     delete context.fn
//     return result;
// }

10.手写bind

https://github.com/mqyqingfeng/Blog/issues/12

难点:传入参数–arguments

当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。

Function.prototype.bind2 = function (context) {
    // 如果调用bind的不是函数
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var self = this;
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);
    var fNOP = function () {};
    var fBound = function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fNOP.prototype = this.prototype;
    // 直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转
    fBound.prototype = new fNOP();
    return fBound;
}

11.柯里化是什么, 有什么作用,及模拟实现

https://github.com/mqyqingfeng/Blog/issues/42

12.原型链,prototype,proto

https://github.com/mqyqingfeng/Blog/issues/2

函数属性prototype----------函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型
对象属性__proto__(除了值类型,一切都是对象)----------指向该对象的原型
constructor----------每个原型都有一个 constructor 属性指向关联的构造函数
实例与原型----------当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
(Object.prototype.proto 的值为 null = Object.prototype 没有原型)
由相互关联的原型组成的链状结构就是原型链

13.变量a和b,如何交换

https://blog.csdn.net/q1056843325/article/details/53223914
交换变量值方案一:临时变量

var t;
t = a; // 首先把a的值存储到临时变量中 
a = b; // 然后b赋值给a 
b = t; // 最后拿出临时变量中的a值赋给b 

交换变量值方案二:让其中一个变量变成一个a和b都有关系的值

a = a + b; // 让a先变成a与b的‘和’(也可以换成a和b的差,一样的) 
b = a - b; // ‘和’减去b巧妙的得到了a的变量值赋予b 
a = a - b; // 再通过‘和’减去a的值得到了b的值赋予a 

或者是下面的变式(差的形式)

a = a - b;
b = a + b;
a = b - a;

交换变量值方案三:位操作 ,通过底层位运算来进行交换变量值

a ^= b;
b ^= a;
a ^= b;

交换变量值方案四:把a先变成了一个对象 ,这个对象保存着应该交换后的键值对 ,最后赋值搞定

a = {a:b,b:a};
b = a.b;
a = a.a;

交换变量值方案五:和上面的方法很像,只不过对象换成了数组

a = [a,b];
b = a[0];
a = a[1];

交换变量值方案六:简单粗暴一行代码交换了a和b的变量值

a = [b,b=a][0];
// 根据运算符优先级,首先执行b=a 
// 此时的b直接得到了a的变量值 
// 然后一步数组索引让a得到了b的值

交换变量值方案七:利用了ES6的解构赋值语法

// 它允许我们提取数组和对象的值,对变量进行赋值 
//(旧版本浏览器不能使用ES6语法)
[a,b] = [b,a];

14.js垃圾回收机制
原理:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。
方法:标记清除,引用计数。
标记清除:js中最常用的垃圾回收方式就是标记清除。当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,而标记为“离开环境”的变量则可以被回收

function func () {
      const a = 1
      const b = 2
      // 函数执行时,a b 分别被标记 进入环境
}
func() // 函数执行结束,a b 被标记 离开环境,被回收

引用计数:另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

function func1 () { 
    const c = {} // 引用类型变量 c的引用计数为 0 
    let d = c // c 被 d 引用 c的引用计数为 1 
    let e = c // c 被 e 引用 c的引用计数为 2 
    d = {} // d 不再引用c c的引用计数减为 1 
    e = null // e 不再引用 c c的引用计数减为 0 将被回收
}

但是引用计数的方式,有一个相对明显的缺点——循环引用

function func2 () {
      let f = {}
      let g = {}
      f.prop = g
      g.prop = f
      // 由于 f 和 g 互相引用,计数永远不可能为 0
}

像上面这种情况就需要手动将变量的内存释放

f.prop = null
g.prop = null

减少JavaScript中的垃圾回收
首先,最明显的,new关键字就意味着一次内存分配,例如 new Foo()。最好的处理方法是:在初始化的时候新建对象,然后在后续过程中尽量多的重用这些创建好的对象。
另外还有以下三种内存分配表达式(可能不像new关键字那么明显了):
{} (创建一个新对象)
[] (创建一个新数组)
function() {…} (创建一个新的方法,注意:新建方法也会导致垃圾收集!!)
(1)、对象object优化
  为了最大限度的实现对象的重用,应该像避使用new语句一样避免使用{}来新建对象。
  {“foo”:”bar”}这种方式新建的带属性的对象,常常作为方法的返回值来使用,可是这将会导致过多的内存创建,因此最好的解决办法是:每一次函数调用完成之后,将需要返回的数据放入一个全局的对象中,并返回此全局对象。如果使用这种方式,就意味着每一次方法调用都会导致全局对象内容的修改,这有可能会导致错误的发生。因此,一定要对此全局对象的使用进行详细的注释和说明。
  有一种方式能够保证对象(确保对象prototype上没有属性)的重复利用,那就是遍历此对象的所有属性,并逐个删除,最终将对象清理为一个空对象。
  cr.wipe(obj)方法就是为此功能而生,代码如下:

// 删除obj对象的所有属性,高效的将obj转化为一个崭新的对象!
cr.wipe = function (obj) {
    for (var p in obj) {
         if (obj.hasOwnProperty(p))
            delete obj[p];
    }
}; 

有些时候,你可以使用cr.wipe(obj)方法清理对象,再为obj添加新的属性,就可以达到重复利用对象的目的。虽然通过清空一个对象来获取“新对象”的做法,比简单的通过{}来创建对象要耗时一些,但是在实时性要求很高的代码中,这一点短暂的时间消耗,将会有效的减少垃圾堆积,并且最终避免垃圾回收暂停,这是非常值得的!
(2)、数组array优化
  将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。

(3)、方法function优化
  方法一般都是在初始化的时候创建,并且此后很少在运行时进行动态内存分配,这就使得导致内存垃圾产生的方法,找起来就不是那么容易了。但是从另一角度来说,这更便于我们寻找了,因为只要是动态创建方法的地方,就有可能产生内存垃圾。例如:将方法作为返回值,就是一个动态创建方法的实例。在游戏的主循环中,setTimeout或requestAnimationFrame来调用一个成员方法是很常见的,例如:

setTimeout(
    (function(self) {                    
      return function () {
          self.tick();
    };
})(this), 16)

每过16毫秒调用一次this.tick(),嗯,乍一看似乎没什么问题,但是仔细一琢磨,每一次调用都返回了一个新的方法对象,这就导致了大量的方法对象垃圾!为了解决这个问题,可以将作为返回值的方法保存起来,例如:

// at startup
this.tickFunc = (
    function(self) {
      return function() {
          self.tick();
      };
    }
)(this);
// in the tick() function
setTimeout(this.tickFunc, 16);

相比于每次都新建一个方法对象,这种方式在每一帧当中重用了相同的方法对象。这种方式的优势是显而易见的,而这种思想也可以应用在任何以方法为返回值或者在运行时创建方法的情况当中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

callmeCassie

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值