秋招前端面经总结-2

1. 原型、原型链

原型:

原型是js中实现继承的基础,

  • js中每个函数都有一个prototype属性,指向函数的原型对象。该对象上的所有属性和方法都会被实例对象继承。
  • js中任何对象都有__proto__,指向构造函数的原型对象。
  • 对象中有一个constructor指向它的构造函数。
原型链:

通过__proto__(隐式原型)实现实例对象与原型对象之间的连接,直至null,这一条链叫做原型链。

__proto__的指向:

取决于对象创建的实现方式,

  • 字面量方式
var a = {};
console.log(a.__proto__ === Object.prototype); // true
console.log(a.__proto__ === a.constructor.prototype); // true
  • 构造器方式
var A = function() {};
var a = new A();
console.log(a.__proto__ === A.prototype); // true
console.log(a.constructor === A); // true
  • Object.create()方式:constructor属性需要注意
var a1 = {};
var a2 = Object.create(a1);
console.log(a2.__proto__ === a1); // true
console.log(a2.constructor === Object); // true
访问对象属性:
  • 先在自身查找,没有再沿着原型链查找。但是只能读取原型中的属性值,不能设置,否则是在自身添加了一个新的属性。
var a = {age: 10};
Object.prototype.name = 'zhangsan';
console.log(a.name); // 会去原型上找,输出 zhangsan
a.name = 'lisi'; 
// 不会修改Prototype上的name,会在a上添加一个name属性
console.log(a);  // {age: 10, name: 'lisi'}
console.log(Object.prototype.name); // zhangsan
  • hasOwnProperty():判断对象属性是自身的还是继承而来的
    • 在原型上的或者不存在的属性都会返回false
  • for in 循环遍历对象的所有可枚举属性,包括原型上的可枚举属性
  • isPrototypeOf 用来判断一个对象是否是另一个对象的原型。(依据原型链来判断)
  • Object.setPrototypeOf/getPrototypeof() 设置/获取一个对象的原型

2. 深拷贝与浅拷贝

深拷贝的方法:

  • 递归拷贝所有层级属性
function deepClone(obj) {
    let objClone = Array.isArray(obj) ? [] : {};
    if (obj && typeof obj === 'object') {
        for(key in obj) {
            if (obj.hasOwnProperty(key)) {
                if (obj[key] && typeof obj[key] === 'object') {
                    objClone[key] = deepClone(obj[key]);
                } else {
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
}
  • JSON.parse与JSON.stringify
function deepClone(obj) {
    let _obj = JSON.stringify(obj);
    let objClone = JSON.parse(_obj);
    return objClone;
}
  • JQuery的extend方法
$.extend(deep, target, obj1, objN)

3. 跨域请求

由于浏览器的同源策略导致的。只有当协议、域名、端口号三者完全一致时才是同源。

  • JSONP跨域:利用script标签没有跨域限制
    • 只支持GET请求
    • 服务器端返回数据格式:回调函数名(数据) ‘callback(data)’
    • callback函数要绑定在window上
    • 无法判断请求是否失败,一般用超时时间
// 手写jsonp
function jsonp(url, data, cb) {
    let dataString = url.indexOf('?') === -1 ? '?' : '&';
    let callbackName = 'jsonpCallback';
    url += `${dataString}callback=${callbackName}`;
    if (data)
        for(let i in data) {
            url += `&${i}=${data[i]}`;
        }
    }
    var script = document.createElement('script');
    script.src = url;
    window[callbackName] = (result) => {
        delete window[callbackName];
        document.body.removeChild(script);
        cb(result);
    };
    script.onerror = function(error) {
        document.body.removeChild(script);
    };
    document.body.appendChild(script);
}
  • CORS 跨域资源共享
    • 后端设置响应头字段 Access-Control-Allow-Origin,一般设为*
    • Access-Control-Allow-Origin为*时http请求不会带上cookie
    • 带上cookie的话,需要前后端都设置Access-Control-Allow-Credentials,并且后端Access-Control-Allow-Origin不能为*,需要指定具体的源
  • 代理:使用nginx将请求转发到后端域名上,前端正常发请求
// nginx配置
server {
    listen 9090; // 监听9090端口
    server_name localhost; // 设置域名
    location ^~ /api {
        proxy_pass http://localhost:9871;
    }
    // localhost:9090/api 这种请求都会转发到服务器地址
} 
  • document.domain + iframe: 限于主域名相同,子域名不同的跨域场景;
    • 将父子页面都通过js强制设置document.domain为相同的主域名,实现同域。
  • window.name + iframe:window.name 在不同页面加载后依然存在
    • 跨域的数据通过iframe的window.name从外域传递到本域;
    • window.name属性仅对相同域名的frame 可访问
// a.html http://www.domain1.com/a.html
var crossDomain = function(url, cb) {
    var state = 0;
    var iframe = document.createElement('iframe');
    // 加载跨域页面
    iframe.src = url;
    // 触发onload事件
    iframe.onload = function() {
        if (state === 0) {
            // 第一次onload,留存数据于window.name,切换到同域页面
            iframe.contentWindow.location = '代理页面地址,与a.html同域';
            state = 1;
        } else {
            // 第二次onload
            cb(iframe.contentWindow.name);
            // 删除iframe
            iframe.contentWindow.close();
            document.body.removeChild(iframe);
        }
    }
}

// b.html http://www.domain2.com/b.html
// 该页面设置window.name
window.name = '数据';
  • postMessage跨域:
    • postMessage(data, origin)
    • 页面监听message事件
// http://www.domain1.com/a.html
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;">
var iframe = document.querySelector('#iframe');
iframe.onload = function() {
    var data = {};
    // 向domain2传送跨域数据
    iframe.contentWindow.postMessage(data, 'http://www.domain2.com');
}
// 接收domain2返回的数据
window.addEventListener('message', function(e) {
    console.log(e.data);
}, false);

// http://www.domain2.com/b.html
// 接收domain1的数据
window.addEventListener('message', function(e) {
    var data2 = e.data;
    // 处理数据,然后发送至domain1
    data2.property1 = 'domain2';
    window.parent.postMessage(data2, 'http://www.domain1.com');
}, false);

4. 箭头函数

与普通函数的不同点:

  • 没有arguments对象,用rest参数(…)代替
  • 没有prototype属性
  • 不绑定this,捕获其执行上下文中的this值,在定义时确定,不会改变
  • 箭头函数是匿名函数,不能使用new操作符

5. 数组去重

  • ES6中利用Set去重
function newArr(arr) {
    return Array.from(new Set(arr));
}
  • for循环嵌套,使用splice去重
function newArr(arr) {
    for(var i = 0; i < arr.length; i++) {
        for(var j = i + 1; j < arr.length; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1);
                j--;
            }
        }
    }
    return arr;
}
  • 建一个新数组,使用indexOf去重
function newArr(arr) {
    var newArr = [];
    for(var i = 0; i < arr.length; i++) {
        if(newArr.indexOf(arr[i]) === -1) {
            newArr.push(arr[i]);
        }
    }
    return newArr;
}
  • 利用filter去重
function newArr(arr) {
    var newArr = arr.filter((item, index, self) => {
        // 找在数组中第一次出现的元素
        self.indexOf(item) === index;
    });
}
对象数组的去重

已知对象的某一个属性id去重:

function newArr(objArr) {
    var obj = {};
    var newObjArr = [];
    for(var i = 0; i < objArr.length; i++) {
        if(!obj[objArr[i].id]) {
            newObjArr.push(objArr[i]);
            obj[objArr[i].id] = true;
        }
    }
    return newObjArr;
}

6. new操作符

使用new操作符产生一个实例的过程:var p = new Person()

  • 创建一个空对象
  • 构造函数中的this指向该空对象
  • 将该对象的原型指向构造函数的prototype属性
  • 将该对象的constructor属性指向构造函数
  • 执行构造函数内部代码,赋予该对象对应的属性

在new的执行过程中,

  • 如果有显式返回值的话:
  • 构造函数返回结果是引用类型,就返回该值
  • 否则就返回一个构造函数的实例。

没有显式返回值,则返回this,即构造函数的实例对象。

实现new运算符:
function MyNew(func) {
    return function() {
        var obj = {};
        obj.constructor = func;
        obj.__proto__ = func.prototype;
        func.apply(obj, arguments);
        return obj;
    }
}
new调用与直接调用构造函数的区别:
  • 函数返回值是非引用类型
function Person(name, age) {
    this.name = name;
    this.age = age;
    // 显式返回非引用类型
    return 'test';
}
var p1 = new Person('zhangsan', 20); // p1是new出来的一个Person实例
var p2 = Person('zhangsan', 20);
console.log(p1.name); // zhangsan
console.log(p2.name); // undefined
console.log(p2); // test
  • 函数返回值是引用类型:两者返回结果是一样的,都是执行显式的return语句
function Person(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    this.test = 'test';
    return o;
}
var p1 = new Person('zhangsan', 20);
var p2 = Person('zhangsan', 20);
// p1不是Person的实例,取不到实例上的test属性
console.log(p1.test); // undefined
console.log(p2.name); // zhangsan

改造构造函数使得直接调用也返回构造函数的实例:

function Person(name, age) {
    // 判断是直接调用还是new调用
    if(!(this instanceof Person)) {
        var p1 = new Person(name, age);
        return p1;
    } else {
        this.name = name;
        this.age = age;
    }
}

7. call/apply/bind

三个方法的作用是用来改变this的指向。

  • call/apply唯一的不同在于传递参数的形式
    • call传递的参数为按顺序依次排列
    • apply传递的参数为数组形式
  • bind参数形式与call一致,但是bind返回一个函数以便后续调用。call/apply是直接执行。
// 实现call
Function.prototype.call = function(context) {
    context = context || window;
    let args = [...arguments].slice(1);
    context.fn = this;
    let result = context.fn(...args);
    delete context.fn;
    return result;
}
// 实现apply
Function.prototype.apply = function(context) {
    context = context || window;
    context.fn = this;
    let result;
    if (arguments[1]) {
        result = context.fn(...arguments[1]);
    } else {
        result = context.fn();
    }
    delete context.fn;
    return result;
}
bind的实现
Function.prototype.bind = function(context) {
    context = context || window;
    let self = this;
    let args = [...arguments].slice(1);
    return function() {
        self.apply(context, args.concat(Array.prototype.slice.call(arguments)));
    };
}
bind多次绑定只会第一次生效:
var one = function(){
    console.log(this.x);
}
var two = {
    x: 1
}
var three = {
    x: 2
}
var fn = one.bind(two).bind(three);
fn(); // 1
改造bind使得多次绑定生效
let preBind = Function.prototype.bind;
Function.prototype.bind = function() {
    var fn = typeof this.__bind__ === 'function' ? this.__bind__ : this;
    var bindFn = preBind.apply(fn, arguments);
    Object.defineProperty(bindFn, '__bind__', {
        value: fn
    });
    return bindFn;
}

8. 变量提升

js执行前先进行‘预编译’,在预编译期间主要完成以下两件事:

  • 声明所有的var变量
  • 解析定义式函数语句

变量的提升是在预编译阶段完成的。

  • 使用var定义变量时,会将声明提升到所在作用域的顶端去执行,到代码所在位置赋值。
  • 函数也会提升,不过是将整个函数代码块提升到它所在作用域的最开始。
  • 函数提升优先级高于变量提升。

只有var定义的变量会提升,ES6中的let和const声明的变量会产生块级作用域,形成暂时性死区。

function test() {
    console.log(test);
    let test = 10;
}
test(); //  Cannot access 'test' before initialization

9. requestAnimationFrame

由于使用setTimeout实现动画效果可能会造成丢帧的现象(执行步调和屏幕刷新步调不一致),可以使用requestAnimationFrame。

  • 由系统来决定回调函数的时机,能保证回调函数在屏幕每一次的刷新间隔中只被执行一次。
  • 在隐藏或不可见的元素中,不会执行动画操作
var progress = 0;
function render() {
    progress += 1;
    if(progress < 100) {
        // 动画未结束前,递归渲染
        window.requestAnimationFrame(render);
    }
}
// 第一帧渲染
window.requestAnimationFrame(render);

10. js中浮点数计算精度的问题

  • 计算机存储的数是二进制的,在进行运算时会将浮点数转换成二进制
  • 对于无限循环的小数,计算机会进行舍入处理
  • 进行双精度浮点数的小数部分最多支持52位

11. 实现String.format方法

String.prototype.format = function() {
    var args = [...arguments];
    var self = this;
    var reg = /{+(\d+)}+/g;
    return self.replace(reg, function(matchStr, index) {
        return typeof args[index] !== 'undefined' ? args[index] : '';
    });
}
'hello {0}, welcome to {1}.'.format('Lily', 'BeiJing');
// hello Lily, welcome to BeiJing

11. 前端安全

XSS:跨站脚本攻击

通过HTML注入插入恶意的脚本,在用户浏览网页时,控制用户浏览器的一种攻击。
防御:

  • httpOnly:带有httpOnly属性的cookie不可以通过js脚本去访问
  • 输入检查:让一些基于特殊字符的攻击失效
  • 输出检查:在变量输出到HTML页面时,使用编码或转义的方式来防御
CSRF:跨站请求伪造

本质是攻击者利用用户身份操作用户账户的一种攻击方式。
在这里插入图片描述
防御:

  • 添加验证码:发请求的时候要求输入验证码强制用户与应用进行交互
  • Referer头字段验证:利用HTTP的Referer头字段来判断请求来源是否合法
  • Token:给请求添加一个随机的Token参数,攻击者则无法构造出相同的URL实施攻击

12. js的垃圾回收机制

为了防止内存泄露(已经不需要某块内存时这块内存还存在着),垃圾回收机制间歇的寻找不再使用的变量,释放内存。

js中主要有两种垃圾回收方式:

标记清除:
垃圾收集器给所有变量加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上标记的即为需要回收的变量。
引用计数:
跟踪一个值的引用次数,当该值引用次数为0时就会被回收。不安全,会引起内存泄露,主要是由于不能解决循环引用的问题。

循环引用
function test() {
    var a = {};
    var b = {};
    a.prop = b;
    b.prop = a;
}
// 执行完test之后,a和b的引用次数都为2,无法被释放

垃圾回收时会停止响应其他的操作。优化垃圾回收机制:

  • 分代回收:区分临时和永久对象;多回收临时对象区,少回收持久对象区。
  • 增量回收:将垃圾回收分解为多个部分,各部分分别执行。这需要额外的标记来跟踪变化。
  • 空闲时间收集:垃圾回收器只在CPU空闲的时候运行,以减少对执行的可能影响
常见的内存泄露原因:
  • 全局变量
  • 遗忘的定时器和回调
function fn() {
	return {
        a: 'test'
    }
}
var obj = fn();
setInterval(function() {
    var testDom = document.getElementById("test")
    if(testDom) {
        testDom.innerHTML = obj;
    }
}, 1000);
// 若没有回收定时器,则定时器函数中的依赖无法被回收,本例中的fn则无法被回收
  • 循环引用
  • 闭包
function test(){
    let ele = document.getElementById('id');
    let id = ele.id; // 引用ele变量id

    ele.onclick = function(){
        console.log(id); // 引用test变量id
    };
}
  • 未清理的DOM引用
    • 对于一个叶子节点的引用额外注意。当删除整个父元素,由于子元素被引用,子元素又保留对于其父元素的引用,导致整个父元素都无法被回收
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值