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引用
- 对于一个叶子节点的引用额外注意。当删除整个父元素,由于子元素被引用,子元素又保留对于其父元素的引用,导致整个父元素都无法被回收