- 1. JavaScript 创建对象的几种方式?
- 2. JavaScript 继承的几种实现方式?
- 3. 说一下对this的理解。
- 4.什么是Proxy?
- 5. 事件委托是什么?
- 6. 说一下你所理解的闭包
- 7. 说一下你所理解的ajax,如何创建一个ajax?
- 8. 说一下你所理解的同源政策?
- 9. 你是如何解决的跨域问题的?
- 10. 你所理解的JavaScript的事件循环机制是什么?
- 11. 说一下对Object.defineProperty()的理解。
- 12. 说一下图片的懒加载和预加载的理解。
- 13. 请求服务器数据,get和post请求的区别是什么?
- 14. Reflect对象创建的目的是什么?
- 15. require 模块引入的查找方式?
- 16 . 观察者模式和发布订阅模式有什么不同?
- 17. 检查数据类型的方法会几种,分别是什么?
- 18. 谈谈对JSON的了解
- 19. 进行哪些操作会造成内存泄漏?
- 20. 谈谈你所理解的函数式编程。
1. JavaScript 创建对象的几种方式?
在 JavaScript 中虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但是这些方法都有一个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为了解决这些问题,对 JavaScript 对象创建的一些理解和总结。
- 工厂模式
工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("james",9,"student");
var person2 = createPerson("kobe",9,"student");
- 构造函数模式
js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么我们就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此我们可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此我们可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次我们都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。
function createPerson(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = new createPerson("james",9,"student");
var person2 = new createPerson("kobe",9,"student");
- 原型模式
因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
function Person(){}
Person.prototype.name = "james";
Person.prototype.age = 9;
Person.prototype.job = "student";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); // "james"
var person2 = new Person();
person2.sayName(); // "james"
console.log(person1.sayName === person2.sayName) // true
- 组合使用的构造函数模式和原型模式
这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此我们可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
}
var person1 = new createPerson("james",9,"student");
var person2 = new createPerson("kobe",9,"student");
console.log(person1.name); // "james"
console.log(person2.name); // "kobe"
console.log(person1.sayName === person2.sayName); // true
- 动态原型模式
这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName !== "function" ){
Person.prototype.sayName: function(){
alert(this.name);
}
}
}
var person1 = new createPerson("james",9,"student");
person1.sayName(); // "james"
- 寄生构造函数模式
这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = new Person("james",9,"student");
- 较为稳妥的构造函数模式
首先明白稳妥对象指的是没有公共属性,而且其方法也不引用this。稳妥对象最适合在一些安全环境中(这些环境会禁止使用this和new),或防止数据被其他应用程序改动时使用。稳妥构造函数模式和寄生模式类似,有两点不同:一是创建对象的实例方法不引用this,而是不使用new操作符调用构造函数。和寄生构造函数模式一样,这样创建出来的对象与构造函数之间没有什么关系,instanceof操作符对他们没有意义。
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
console.log(this.name);
}
//返回对象
return o;
}
var person1 = Person("james",9,"student");
person1.sayName(); // "james"
2. JavaScript 继承的几种实现方式?
继承是面向对象语言中最重要的一个概念。许多面向对象语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法和签名,而实现继承则继承实际的方法。由于在 JavaScript 中函数没有签名,因此无法实现接口继承,只支持实现继承。
- 原型链继承
原型链继承通过修改子类的原型为父类的实例,从而实现子类可以访问到父类构造函数以及原型上的属性或者方法。
function Parent() {
this.name = 'sunny'
}
Parent.prototype.getName = function() {
return this.name;
}
function Child() {}
// 这里也可以直接写出Child.prototype = Parent.prototype
// 但是这样就不能访问到父类的构造函数的属性了,即this.name
Child.prototype = new Parent()
var child = new Child()
child.getName() // sunny
优点:
实现逻辑简单。缺点:
父类构造函数中的引用类型(比如对象/数组),会被所有子类实例共享。其中一个子类实例进行修改,会导致所有其他子类实例的这个值都会改变。
- 构造函数继承
构造函数继承其实就是通过修改父类构造函数this实现的继承。我们在子类构造函数中执行父类构造函数,同时修改父类构造函数的this为子类的this。
function Parent() {
this.name = ['sunny']
}
function Child() {
Parent.call(this)
}
var child = new Child()
child.name.push('rain')
console.log(child)//{name:["sunny","rain"]}
var child2 = new Child()
console.log(child2)//{name:["sunny"]}
优点:
解决了原型链继承中构造函数引用类型共享的问题,同时可以向构造函数传参(通过call传参)缺点:
所有方法都定义在构造函数中,每次都需要重新创建(对比原型链继承的方式,方法直接写在原型上,子类创建时不需要重新创建方法)
- 组合继承
同时结合原型链继承、构造函数继承就是组合继承了。
function Parent() {
this.name = 'sunny'
console.log("父类")
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this)
this.text = 'rain'
console.log("child")
}
Child.prototype = new Parent()
// 需要重新设置子类的constructor,Child.prototype = new Parent()相当于子类的原型对象完全被覆盖了
Child.prototype.constructor = Child
var child=new Child;
console.log(child)//{name:"sunny",text:"rain"}
优点:
同时解决了构造函数引用类型的问题,同时避免了方法会被创建多次的问题缺点:
父类构造函数被调用了两次。同时子类实例以及子类原型对象上都会存在name属性。虽然根据原型链机制,并不会访问到原型对象上的同名属性,但总归是不完美。
- 寄生组合继承
寄生组合继承其实就是在组合继承的基础上,解决了父类构造函数调用两次的问题。
function Parent() {
this.name = 'sunny'
console.log("父类")
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this)
this.text = 'rain'
console.log("child")
}
// 仔细看这个函数的实现
inherit(Child, Parent)
function inherit(child, parent) {
var prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
// 这个函数的作用可以理解为复制了一份父类的原型对象
// 如果直接将子类的原型对象赋值为父类原型对象
// 那么修改子类原型对象其实就相当于修改了父类的原型对象
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var child=new Child
console.log(child)//{name:"sunny","text":"rain"}
优点:
这种方式就解决了组合继承中的构造函数调用两次,构造函数引用类型共享,以及原型对象上存在多余属性的问题。是推荐的最合理实现方式(排除ES6的class extends继承哈哈哈)。缺点:
暂时没有啥特别的缺点
- ES6继承
ES6提供了class语法糖,同时提供了extends用于实现类的继承。这也是项目开发中推荐使用的方式。使用class继承很简单,代码也很直观:
class Parent {
constructor() {
this.name = 'sunny'
}
getName() {
return this.name
}
}
class Child extends Parent {
constructor() {
super()
this.text = 'rain'
}
}
const child = new Child()
child.getName() //"sunny"
3. 说一下对this的理解。
this 的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象。
- 作为普通函数在全局环境中被调用
在全局环境里面,this永远指向window,因此在全局环境里作为普通函数被调用的时候,this也指向 window(仅指在浏览器环境下,赞不考虑node)。
var name = "sunny";
function fn() {
console.log(this); //window
console.log(this.name); //sunny
}
fn();
这里,fn 其实是作为 window 的一个方法被调用的,而 name 也是 window 的一个属性,因此 fn() 实际上就是 window.fn()。
- 作为对象的属性被调用
如果函数作为一个对象的属性方法,并且被调用的时候,this 就指向这个对象。
var name = 'window';
var person = {
name: 'person',
sayName: function() {
console.log(this.name);
}
};
var sayNameWin = person.sayName;
person.sayName(); //person
sayNameWin(); //window 作为 window 的方法被调用的
在这里,sayName 方法是作为 person 的一个属性方法被调用的,因此指向 person,但是 sayNameWin 方法却是作为 window 的一个属性方法被调用的,因此 console.log 的值是 window。我们再看一个变形。
var person1 = {
name: 'person1',
sayName: function() {
console.log(this.name)
}
}
var person2 = {
name: 'person2',
sayName: person1.sayName
}
person2.sayName(); //person2 作为 person2 的属性方法被调用
但是当在在对象方法中再定义函数,这时候 this 指向是 window 。
var name = 'window';
var person = {
name: 'person',
sayName: function () {
function fn(){
console.log(this); //window对象
console.log(this.name); //window
}
fn();
}
}
person.sayName();
如果想让 this 指向 person 的话,只需要用 that 保存下来 this 的值即可,也可以使用 apply 等改变 this。
var name = 'window';
var person = {
name: 'person',
sayName: function () {
var that = this;
function fn(){
console.log(that); // {name: "person",sayName:F}
console.log(that.name); //person
}
fn();
}
}
person.sayName();
- 作为构造函数被调用
作为构造函数被调用的时候,this 代表它即将 new 出来的对象。
function Person(name) {
this.name = name;
console.log(this); //Person {name: "person"}
}
var person = new Person('person');
console.log(person.name); //person
如果不加 new,表示即作为普通函数调用,this指向 window
function Person(name) {
this.name = name;
console.log(this); //window对象
}
Person('person');
console.log(window.name); //person
- 作为 call/apply/bind方法被调用的时候指向传入的值 ```javascript var person = { name: ‘person’ }; function fn() { console.log(this); // {name: “person”} console.log(this.name); //person }
fn.apply(person);
- 严格模式("use static")
在严格模式下,在全局环境中执行函数调用的时候 this 并不会指向 window 而是会指向 undefined。
```javascript
'use strict';
function person() {
console.log(this); //undefined
};
person();
- setTimeout、setInterval中的this
《 javascript 高级程序设计》中写到:”超时调用的代码都是在全局执行域中执行的”。setTimeout/setInterval 执行的时候,this 默认指向 window 对象,除非手动改变 this 的指向。
var name = 'window';
function Person(){
this.name = 'person';
this.sayName=function(){
console.log(this); //window对象
console.log(this.name); //window
};
setTimeout(this.sayName, 10);
}
var person=new Person();
在这里如果想改变 this,可是使用 apply/call/bind 等,也可以使用 that 保存 this.
setTimeout 中的回调函数在严格模式下也指向 window 而不是 undefined。因为 setTimeout 的回调函数如果没有指定的 this ,会做一个隐式的操作,将全局上下文注入进去,不管是在严格还是非严格模式下。
'use strict';
function person() {
console.log(this); //windowd对象
}
setTimeout(person, 0);
构造函数 prototype 属性
var name = 'window';
function Person(){
this.name = 'person';
}
Person.prototype.sayName = function () {
console.log(this); // Person {name: "person"}
console.log(this.name); // person
}
var person = new Person();
person.sayName();
在 Person.prototype.sayName 函数中,this 指向的 person 对象。即便是在整个原型链中,this 也代表当前对象的值。
eval函数
在 eval 中,this指向当前作用域的对象。
var name = 'window';
var person = {
name: 'person',
getName: function(){
eval("console.log(this.name)");
}
}
person.getName(); //person
var getNameWin=person.getName;
getNameWin(); //window
// 在这里,和不使用 Eval ,作为对象的方法调用的时候得出的结果是一样的。
- 箭头函数
它里面的this
是由外层作用域来决定的,且指向函数定义时的this而非执行时。因为箭头函数没有 this,因此它自身不能进行new实例化,同时也不能使用 call, apply, bind 等方法来改变 this 的指向。
var person = {
name: 'person',
sayName: function() {
var fn = () => {
return () => {
console.log(this); //{name: "person",sayName:F}
console.log(this.name); //person
}
}
fn()();
}
}
person.sayName();
4.什么是Proxy?
Proxy概念
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});
上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
下面是另一个拦截读取属性行为的例子。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
上面代码中,作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35。
注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果handler没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target。
一个技巧是将 Proxy 对象,设置到object.proxy属性,从而可以在object对象上调用。
var object = { proxy: new Proxy(target, handler) };
Proxy 实例也可以作为其他对象的原型对象。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。
同一个拦截器函数,可以设置拦截多个操作。
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
- set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
- has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
- deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
- setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
5. 事件委托是什么?
基本概念
事件委托,通俗地来讲,就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素;一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
举个例子,比如一个宿舍的同学同时快递到了,一种方法就是他们都傻傻地一个个去领取,还有一种方法就是把这件事情委托给宿舍长,让一个人出去拿好所有快递,然后再根据收件人一一分发给每个宿舍同学;在这里,取快递就是一个事件,每个同学指的是需要响应事件的 DOM 元素,而出去统一领取快递的宿舍长就是代理的元素,所以真正绑定事件的是这个元素,按照收件人分发快递的过程就是在事件执行中,需要判断当前响应的事件应该匹配到被代理元素中的哪一个或者哪几个。
事件冒泡
前面提到 DOM 中事件委托的实现是利用事件冒泡的机制,那么事件冒泡是什么呢?
在 document.addEventListener 的时候我们可以设置事件模型:事件冒泡、事件捕获,一般来说都是用事件冒泡的模型;
如上图所示,事件模型是指分为三个阶段:
捕获阶段:在事件冒泡的模型中,捕获阶段不会响应任何事件;
目标阶段:目标阶段就是指事件响应到触发事件的最底层元素上;
冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点),事件代理即是利用事件冒泡的机制把里层所需要响应的事件绑定到外层;
小小优点:
使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。手写代码实现:
一个函数 eventDelegate,它接受四个参数:
- [String] 一个选择器字符串用于过滤需要实现代理的父层元素,既事件需要被真正绑定之上;
- [String] 一个选择器字符串用于过滤触发事件的选择器元素的后代,既我们需要被代理事件的元素;
- [String] 一个或多个用空格分隔的事件类型和可选的命名空间,如 click 或 keydown.click ;
- [Function] 需要代理事件响应的函数;
function eventDelegate (parentSelector, targetSelector, events, foo) {
// 触发执行的函数
function triFunction (e) {
// 兼容性处理
var event = e || window.event;
// 获取到目标阶段指向的元素
var target = event.target || event.srcElement;
// 获取到代理事件的函数
var currentTarget = event.currentTarget;
// 处理 matches 的兼容性
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// 遍历外层并且匹配
while (target !== currentTarget) {
// 判断是否匹配到我们所需要的元素上
if (target.matches(targetSelector)) {
var sTarget = target;
// 执行绑定的函数,注意 this
foo.call(sTarget, Array.prototype.slice.call(arguments))
}
target = target.parentNode;
}
}
// 如果有多个事件的话需要全部一一绑定事件
events.split('.').forEach(function (evt) {
// 多个父层元素的话也需要一一绑定
Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {
$p.addEventListener(evt, triFunction);
});
});
}
6. 说一下你所理解的闭包
- 函数执行会形成一个私有上下文,如果上下文中的某些内容(一般指的是堆内存地址)被上下文以外的一些事物(例如:变量/事件绑定等)所占用,则当前上下文不能被出栈释放「浏览器的垃圾回收机制GC所决定的」 =>“闭包”的机制:形成一个不被释放的上下文
- 保护:保护私有上下文中的“私有变量”和外界互不影响
- 保存:上下文不被释放,那么上下文中的“私有变量”和“值”都会被保存起来,可以供其下级上下文中使用
- 弊端:如果大量使用闭包,会导致栈内存太大,页面渲染变慢,性能受到影响,所以真实项目中需要“合理应用闭包”;某些代码会导致栈溢出或者内存泄漏,这些操作都是需要我们注意的;
或者另外理解
实现闭包的条件:
- 函数嵌套着函数
- 子函数引用父函数的变量或参数
- 子函数被外界所引用
- 父级就形成了闭包环境(父级的执行栈不会被销毁),父级的参数或是变量不会被浏览器垃圾机制给强制回收
- 此时打印父级的函数返回值,会出现scopes下有个closure===>闭包
正规闭包的格式
//正规闭包的模式
function fn(){
var a=10;
function f(){
console.log(a)
}
return f;
}
let ff=fn();//fn就成了闭包环境(执行栈不会被销毁)
//fn中的参数或是变量不会被浏览器垃圾机制给强制回收
console.dir(ff)//scopes下里面有closure(闭包)
//浏览器关闭时才会销毁。
//或重新赋值为null才会销毁。
//ff()会存储现在的值并会累计相加。
此时父级的函数返回值的打印结果:
7. 说一下你所理解的ajax,如何创建一个ajax?
ajax的简介
浏览器与服务器之间,采用http协议通信。用户在浏览器地址栏中输入一个网址或者通过网页表单向服务器提交内容,这时浏览器就会向服务器发送http请求。
1999年,微软公司发布IE浏览器5.0版,第一次引入新功能:允许JavaScript脚本向服务器发起http请求。这个功能当时并没有引起注意,直到2004年Gmail发布和2005年Goole Map发布,才引起广泛关注。2005年2月,ajax这一个词第一次正式提出,它是 Asynchronous JavaScript and XML
的缩写,指的是通过JavaScript的异步通信,从服务器获取XML文档从中提取数据,在更新当前网页的对应部分,而不用刷新整个网页。后来,ajax这个词就成为JavaScript脚本发起http通信的代名词,也就是说,只要用脚本发起通信,就可以叫做ajax通信。W3C也在2006年发布了它的国际标准。
具体来说,ajax包括以下几个步骤:
- 创建 XMLHttpRequest 实例对象
- 发出 HTTP 请求
- 接收服务器传回的数据
- 更新网页数据
概括起来,就是一句话:ajax通过原生的 XMLHttpRequest
对象发出http请求,得到服务器返回的数据后在进行处理。现在,服务器返回的都是json格式的数据,xml格式已经过时了,但是ajax这个名字已经成了一个通用名词,字面含义已经消失了。XMLHttpRequest对象
是ajax的主要接口,用于浏览器于服务器之间的通信。尽管名字里面有 XML和HTTP
,它实际上可以使用多种协议(比如 file或者ftp
),发送任何格式的数据(包括字符串和二进制)。
创建一个ajax的代码:
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}
8. 说一下你所理解的同源政策?
同源策略的概念同源策略
是一种出于浏览器安全方面的考虑而出台的一种策略,它可以保证用户信息的安全,防止恶意的网站窃取。同源策略只允许于与本域下的接口交互,不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。
同源指的是:同协议 同域名 同端口
同源政策主要限制了三个方面:
- 当前域下的js脚本不能够访问其他域下的cookie、localStorage和indexDB
- 当前域下的js脚本不能够访问和操作其他域下的DOM和js对象
-
9. 你是如何解决的跨域问题的?
通过jsonp跨域
通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。
原生实现代码
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
console.log(JSON.stringify(res));
}
</script>
服务端返回如下(返回时即执行全局函数)
handleCallback({"status":true,"user":"admin","age":11})
JQuery ajax:
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {}
});
后端node.js代码示例:
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
jsonp缺点:只能实现get一种请求。
- document.domain + iframe 跨域
此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都是通过js强制设置document.domain为基础主域,就实现了同域。
// 父窗口:(http://www.domain.com/a.html)
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
// 子窗口:(http://child.domain.com/b.html)
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
- location.hash + iframe 跨域
实现原理:a想和b跨域相互通信,通过中间页c来实现。三个页面,不同域之间利用iframe的location.hash传值,相同域之间js访问通信。具体实现:a域:a.html —> b域:b.html —> a域:c.html ,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c和a是同域,所以c可通过parent.parent访问a页面所有对象。
// a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
//b.html:(http://www.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
//c.html:(http://www.domain1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
- window.name + iframe 跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
//a.html:(http://www.domain1.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
//proxy.html:(http://www.domain1.com/proxy....
//中间代理页,与a.html同域,内容为空即可。
//b.html:(http://www.domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个巧妙地绕过了浏览器地跨域访问限制,但同时它又是安全操作。
- postMessage 跨域
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
页面和其打开的新窗口的数据传递
多窗口之间消息传递
页面与嵌套的iframe消息传递
上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为”*”,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。
// a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
// b.html:(http://www.domain2.com/b.html)
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
- 跨域资源共享(CORS)
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:nginx反向代理中设置proxy_cookie_domain 和 NodeJs中间件代理中cookieDomainRewrite参数的设置。
目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。前端设置
原生ajax:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
JQuery ajax:
$.ajax({
...
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
...
});
axios设置
axios.defaults.withCredentials = true
服务端设置
若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
java后台
/*
* 导入包:import javax.servlet.http.HttpServletResponse;
* 接口参数中定义:HttpServletResponse response
*/
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
node.js后台示例:
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var postData = '';
// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.domain1.com', // 允许访问的域(协议+域名+端口)
/*
* 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
*/
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly的作用是让js无法读取cookie
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen('8080');
console.log('Server is running at port 8080...');
- nginx 代理跨域
nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
nginx反向代理接口跨域
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
nginx具体配置:
#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
前端代码示例:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
node.js后台示例
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
- node.js中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。利用node + express + http-proxy-middleware搭建一个proxy服务器
前端代码示例:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
nodejs中间服务器:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
nodejs后台代码可以参考 nginx代码
vue框架跨域
利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
webpack.config.js部分配置:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
- WebSocket协议跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端代码:
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
node.js socket后台:
var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
10. 你所理解的JavaScript的事件循环机制是什么?
javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的”多线程”都是用单线程模拟出来的,一切javascript多线程都是纸老虎! 需要注意的是:JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。
在解释事件循环之前首先先解释一下浏览器的执行线程: 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等 其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程.
关于执行中的线程
主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件。
任务队列
所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列的机制(先进先出的机制)来进行协调。
可以用图片具体的说明:
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
在事件循环中,每进行一次循环操作称为 tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
1.在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行
2.检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
3.更新 render
4.主线程重复执行上述步骤
可以用一张图来说明流程
这里相信有人会想问,什么是 microtasks ?规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task 。
宏任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境) 微任务主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)
setTimeout/Promise 等API便是任务源,而进入任务队列的是由他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中 setTimeout 与 setInterval 是同源的。
11. 说一下对Object.defineProperty()的理解。
对象是由多个名/值对组成的无序的集合。对象中每个属性对应任意类型的值。
定义对象可以使用构造函数或者字面量的形式:
var obj = new Object; //obj = {}
obj.name = "张三"; //添加描述
obj.say = function(){}; //添加行为
除了以上添加属性的方式,还可以使用Object.defineProperty定义新属性或修改原有的属性。
其语法: Object.defineProperty(obj,prop,descript)
其参数说明:
obj:必需,目标对象 prop:必需,需要定义或修改属性的名字 descript:必需,目标属性所拥有的特性
其返回值: 传入函数的对象,即第一个参数
给对象的属性添加特性描述,目前提供两种形式: 数据描述和存取器描述
- 数据描述
当修改或定义对象的某个属性的时候,给这个属性添加一些特性:
var obj = {
test:"hello"
}
//对象已有的属性添加特性描述
Object.defineProperty(obj,"test",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});
//对象新添加的属性的特性描述
Object.defineProperty(obj,"newKey",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});
数据描述中的属性都是可选的,接下来看看每一个属性的作用。
value:属性对应的值,可以是任意类型的值,默认为undefined。 writable:属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false。 enumerable:此属性是否可以被枚举(使用for…in或Object.keys())。设置为true可以被枚举;设置为false不能被枚举。默认为false。 configurable:是否可以删除目标属性或是否可以再次修改属性的特性(目标属性是否可以使用delete删除,目标属性是否可以再次设置特性)。设置为true的话可以删除或可以重新设置;甚至为false的话不能被删除或不可以重新设置特性。默认为false。
- 存取器描述
当使用存取器描述属性的特性的时候,允许设置以下特性属性:
var obj = {};
Object.defineProperty(obj,"newKey",{
get:function (){} | undefined,
set:function (value){} | undefined
configurable: true | false
enumerable: true | false
});
注意:当使用getter或setter方法,不允许使用writable和value这两个属性 getter 是一种获得属性值的方法 setter 是一种设置属性值的方法
var obj = {};
var initValue = 'hello';
Object.defineProperty(obj,"newKey",{
get:function (){
//当获取值的时候触发的函数 return initValue;
},
set:function (value){
//当设置值的时候触发的函数,设置的新值通过参数value拿到
initValue = value;
return initValue;
}
});
//获取值
console.log( obj.newKey ); //hello
//设置值
obj.newKey = 'change value';
console.log( obj.newKey ); //change value
get或set不是必须成对出现的,任写其一就可以。如果不设置方法,则get和set的默认值为undefined; configurable和enmerable 同上面的方法 兼容性:在ie8下只能在DOM对象上使用,尝试在原生的对象使用Object.defineProperty()会报错。
12. 说一下图片的懒加载和预加载的理解。
懒加载
什么叫做懒加载?
懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚下哦那个到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。适用于图片很多,页面很长的电商网站场景中。
为什么要用懒加载?
能提升用户的体检。不妨设想下,用户打开像手机淘宝长页面的时候,如果页面上所有的图片都需要加载,由于图片数目较大,等待时间很长,用户难免会心生抱怨,这就严重影响用户体验
减少无效资源的加载,这样能明显减少服务器的压力和流量,也能够减少浏览器的负担
防止并发加载的资源过多会阻塞js的加载,影响网站的正常使用。
懒加载的原理
首先将页面上的图片的src属性设为空字符串,而图片的真实路径则设置在 data-original 属性中,当页面滚动的时候需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区域就将鱼片的src属性设置为data-original的值,这样就可以实现延迟加载
懒加载实现步骤
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lazyload</title>
<style>
.image-item {
display: block;
margin-bottom: 50px;
/* 一定记得设置图片高度 */
height: 200px;
}
</style>
</head>
<body>
<img src="" class="image-item" lazyload="true" data-original="images/1.png" />
<img src="" class="image-item" lazyload="true" data-original="images/2.png" />
<img src="" class="image-item" lazyload="true" data-original="images/3.png" />
<img src="" class="image-item" lazyload="true" data-original="images/4.png" />
<img src="" class="image-item" lazyload="true" data-original="images/5.png" />
<img src="" class="image-item" lazyload="true" data-original="images/6.png" />
<img src="" class="image-item" lazyload="true" data-original="images/7.png" />
<img src="" class="image-item" lazyload="true" data-original="images/8.png" />
<img src="" class="image-item" lazyload="true" data-original="images/9.png" />
<img src="" class="image-item" lazyload="true" data-original="images/10.png" />
<img src="" class="image-item" lazyload="true" data-original="images/11.png" />
<img src="" class="image-item" lazyload="true" data-original="images/12.png" />
<script>
var viewHeight = document.documentElement.clientHeight//获取可视区高度
function lazyload() {
var eles = document.querySelectorAll('img[data-original][lazyload]')
Array.prototype.forEach.call(eles, function (item, index) {
var rect
if (item.dataset.original === "") return
rect = item.getBoundingClientRect()// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
if (rect.bottom >= 0 && rect.top < viewHeight) {
(function () {
var img = new Image()
img.src = item.dataset.original
img.onload = function () {
item.src = img.src
}
item.removeAttribute("data-original")//移除属性,下次不再遍历
item.removeAttribute("lazyload")
})()
}
})
}
lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll", lazyload)
</script>
</body>
</html>
预加载
什么是预加载?
资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到的时候直接从缓存取出资源。
为什么要用预加载?
在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待的事件。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现一片空白,直到所有内容加载完毕。
实现预加载的几种办法
使用HTML标签
<img src="http://hbimg.huabanimg.com/39b716cc64cd7af2131043b50b427d3597249020bbc34-wPobUM_fw236/format/webp" style="display:none" />
使用image对象
<sctipt src="./myPreload.js"></script>
//myPreload.js文件
var image=new Image()
image.src="http://hbimg.huabanimg.com/39b716cc64cd7af2131043b50b427d3597249020bbc34-wPobUM_fw236/format/webp"
使用XMLHttpRequest对象,虽然存在跨域问题,但会精细控制预加载内容
var xmlhttprequest = new XMLHttpRequest();
xmlhttprequest.onreadystatechange = callback;
xmlhttprequest.onprogress = progressCallback;
xmlhttprequest.open("GET", "http://hbimg.huabanimg.com/39b716cc64cd7af2131043b50b427d3597249020bbc34-wPobUM_fw236/format/web", true);
xmlhttprequest.send();
function callback() {
if (xmlhttprequest.readyState == 4 && xmlhttprequest.status == 200) {
var responseText = xmlhttprequest.responseText;
console.log(responseText)
} else {
console.log("Request was unsuccessful:" + xmlhttprequest.status);
}
}
function progressCallback(e) {
e = e || event;
if (e.lengthComputable) {
console.log("Received" + e.loaded + "of" + e.total + "bytes")
}
}
使用PreloadJS库
PreloadJS提供了一种预加载内容的一致方式,以便在HTML应用程序中使用。预加载可以使用HTML标签以及XHR来完成。默认情况下,PreloadJS会尝试使用XHR加载内容,因为它提供了对进度和完成事件的更好支持,但是由于跨域问题,使用基于标记的加载可能会更好。
//使用preload.js
var queue = new createjs.LoadQueue();//默认是xhr对象,如果是new createjs.LoadQueue(false)是指使用HTML标签,可以跨域
queue.on("complete", handleComplete, this);
queue.loadManifest([
{ id: "myImage", src: "http://hbimg.huabanimg.com/39b716cc64cd7af2131043b50b427d3597249020bbc34-wPobUM_fw236/format/web" },
{ id: "myImage2", src: "http://hbimg.huabanimg.com/39b716cc64cd7af2131043b50b427d3597249020bbc34-wPobUM_fw236/format/web" }
]);
function handleComplete() {
var image = queue.getResuLt("myImage");
document.body.appendChild(image);
}
两者方式都是提高页面性能有效的方法,两者主要区别是一个是迟缓甚至不加载,一个是提前加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
13. 请求服务器数据,get和post请求的区别是什么?
两者的联系:
get和post是http请求的两种方式,底层都是用TCP/IP协议进行通信的。用户发出HTTP请求的大致过程:
用户使用电脑(或手机)发出请求,此时电脑(或手机)就是一个客户端,有自己的ip,通过socket将请求包发出
请求数据包会通过TCP协议,通过网络传输给远程服务端ip,服务端ip收到请求包之后,解析并处理请求包
最后服务端会通过TCP协议将处理结果返回给客户端ip,用户可以查看到想要的响应数据。
get和post本质并无区别,只是被http规定了不同的行为和方式。之所以有get和post区分,是因为它们底层数据的传输都是基于TCP协议,如果不做区分,网络中都是某一种服务类别,难免会混乱。为了避免这种情况发生,http协议诞生了。
HTTP给网络运输设定了好几个服务类别,有GET\POST\PUT\DELETE等,HTTP规定,当执行GET请求的时候,要给请求包贴上GET的标签(设置method为GET),而且要求把传送的数据放在url中来方便记录。如果是POST请求,在请求包贴上POST的标签
两者区别:
get请求在浏览器后退时无害,不发送请求。post在浏览器后退时会再次发送请求
get参数通常放在url后面传递,post则通常放在Request body中传递。但实际上,get也可以用body少量传值,post也可以在url中少量传值,这在技术上是完全行的通的,只是不符合http的规定
get比post更不安全,因为参数直接暴露在URL中,所以不能用来传递敏感信息
get在url中传输参数有长度限制,post没有。get之所以会限制请求长度,是因为url请求数据量太大对浏览器和服务器都是很大的负担,处理起来有成本,所以浏览器和服务器对单次访问都做了限制。(大多数)浏览器通常会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。超过的部分,概不处理。虽然get可以带request body,也不保证一定被接收到。所以此处的不同,是因为http的规定和浏览器/服务器的限制,导致它们在应用过程中体现出一些不同。所以如果请求参数可能很长很多的话,直接使用post就可以啦(如果用get超过限制,参数传不过去,会报null)!
get产生一个TCP数据包,post产生两个TCP数据包。对于get方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于post,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。也就是post请求,第一次将header发送过去,确认服务器和网络没问题可以服务,才会将真正的data数据提交。因为post需要两步,时间上消耗的要多一些,看起来get比post更有效。
所以Yahoo团队有推荐用get替换post来优化网站性能。但是这个是一个坑,需要谨慎对待,为什么呢?解释原因如下:
get和post都有自己的语义,不能随便混用。get从指定的支援获取数据,post是向指定的资源提交数据(使用场景上面的不同)
在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。也就是网络好的话get和post请求效率基本一样,网络不好的时候post对验证请求数据完整性更有优势
并不是所有浏览器都会在post中发送两次包,常用的Firefox就只发送一次
14. Reflect对象创建的目的是什么?
Reflect对象的设计目的:
1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放在Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法
2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj,name,desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj,name,desc)则会返回false
//老写法
try{
Object.defineProperty(target,property,attributes);
//successs
}catch(e){
//failure
}
//新写法
if(Reflect.defineProperty(target,property,attributes)){
//success
}else{
//failure
}
3. 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj 和 delete obj[name],而Reflect.has(obj,name)和Reflect.deleteProperty(obj,name)让它们变成了函数行为。
//老写法
'assign' in Object //true
//新写法
Reflect.has(Object,'assign')//true
4. Reflect 对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Proxy(target,{
set:function(target,name,value,receiver){
var success=Reflect.set(target,name,value,receiver);
if(success){
console.log('property'+name+'on'+target+'set to '+value);
}
return success;
}
});
上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。举另一个栗子:
var loggedObj=new Proxy(obj,{
get(target,name){
console.log('get',target,name);
return Reflect.get(target,name);
},
deleteProperty(target,name){
console.log('delete' + name);
return Reflect.deleteProperty(target,name);
},
has(target,name){
console.log('has' + name);
return Reflect.has(target,name);
}
});
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
有了Reflect对象之后,很多操作会更易读。
//老写法
Function.prototype.apply.call(Math.floor,undefined,[1.75])//1
//新写法
Reflect.apply(Math.floor,undefined,[1.75])//1
15. require 模块引入的查找方式?
当Node遇到require(X)时,按照下面的顺序处理:
1.如果 X 是内置模块(比如 require('http'));
· 返回该模块
· 不再继续执行
2.如果X以"./"或者"/"或者"../"开头
· 根据X所在的父模块,确定X的绝对路径
· 将X当成文件,依次查找下面文件,只要其中一个存在,就返回该文件,不再继续执行。
X
X.js
X.json
X.node
· 将X当成目录,依次查找下面文件,只要其中有一个存在就返回该文件,不再继续执行
X/package.json (main字段)
X/index.js
X/index.json
X/index.node
3.如果X不带路径
· 根据X所在的父模块,确定X可能的安装目录
· 依次在每个目录中,将X当成文件或目录名加载
4. 抛出 "not found"
16 . 观察者模式和发布订阅模式有什么不同?
观察者模式
所谓观察者模式,其实就是为了实现松耦合(loosely coupled)
对于观察者模式,我们仅需要维护一个可观察对象即可,即一个Observable实例,当数据变化时,它只需维护一套观察者(Observer)的集合,这些Observer实现相同的接口,Subject只需要知道,通知Observer时,需要调用哪一个统一方法就好啦。
发布订阅模式
在发布订阅模式里,发布者并不直接通知订阅者。换句话说:发布者和订阅者彼此互不认识。那么的话是怎样进行交流的呢?
答案是:通过第三者触发,也就是在消息队列里,发布者和订阅者通过事件名称来联系,匹配上后直接执行对应订阅者的方法即可。
发布/订阅者模式与观察者模式主要有以下几个不同点:
在观察者模式中,主体维护观察者列表,因此主体(被观察者)知道当状态发生变化时如何通知观察者。然而,在发布/订阅者中,发布者和订阅者不需要相互了解。它们只需在中间层消息代理(或消息队列)的帮助下进行通信。
在发布/订阅者模式中,组件与观察者模式完全分离。在观察者模式中,主体和观察者松散耦合。
观察者模式主要是以 同步方式 实现的,即当发生某些事件时,主体调用其所有观察者的适当方法。发布/订阅服务器模式主要是以 异步方式 实现(使用消息队列)
发布/订阅者模式更像是一种跨应用程序模式。发布/订阅服务器可以驻留在两个不同的应用程序中。它们中的每一个都通过消息代理或消息队列进行通信。
17. 检查数据类型的方法会几种,分别是什么?
判断一个变量的类型
JavaScript基本数据类型有5种,String、Number、Boolean、null、undefined。用户定义的类型(Object)并没有类的声明,因此继承关系只能通过构造函数和原型链来检查。如果你要判断的是基本数据类型或JavaScript内置对象,使用toString;如果要判断的是自定义类型,请使用instanceof
- typeof
typeof操作符返回的是类型字符串,只能判断基本数据类型,所有对象(引用数据类型)的typeof都是”Object”,不能用于检测用户自定义类型。
需要注意的是typeof null 的结果是“Object”
也侧面反映了null的语义,是一个空指针表示空对象(所谓的知名bug存在)
- instanceof
instanceof操作符用于检查某个对象的原型链是否包含某个构造函数的prototype的属性。
obj instanceof Widget(类名或者函数名称)
obj的原型链上有很多对象(成为隐式原型),比如:obj.__proto__
,obj.__proto__.__proto__
…,如果这些对象存在一个p===Widget.prototype,那么instanceof结果为true,否则为false。
//直接原型关系
function Animal(){}
(new Animal) instanceof Animal //true
//原型链上的间接原型
function Cat(){}
Cat.prototype=new Animal
(new Cat) instanceof Animal //true
instanceof 可以用来检测内置对象,比如Array、RegExp、Object、Function:
[1,2,3] instanceof Array //true
/123/ instanceof RegExp //true
({}) instanceof Object //true
(function(){}) instanceof Function //true
instanceof 对基本数据类型不起作用,因为基本数据类型没有原型链。
3 instanceof Number //false
true instanceof Boolean //false
"aaa" instanceof String //false
但是可以这样写就会有效
new Number(3) instanceof Number //true
new Boolean(true) instanceof Boolean //true
new String("aaa") instanceof String //true
- constructor
constructor属性返回一个指向创建该对象原型的函数引用。需要注意的是:该属性的值是那个函数的本身:
function Animal (){}
var a=new Animal
a.constructor === Aniaml //true
constructor不适合用来判断变量类型。首先因为它是一个属性,所以非常容易被伪造:
var a=new Animal
a.constructor = Array
a.constructor === Animal //false
constructor指向的是最初创建当前对象的函数,是原型链__proto__
最上层的那个方法。
function Cat(){}
Cat.prototype=new Animal
function BadCat(){}
BadCat.prototype=new Cat
(new BadCat).constructor === Animal //true
Animal.constructor === Function //true
与instanceof类似,constructor只能用于检测对象,对于基本数据类型无能为力。而且因为 constructor 是对象属性,在基本数据类型上调用会抛出TypeError异常
:
null.constructor //TypeError!!
undefined.constructor //TypeError!!
和 instanceof 不同的是:在访问基本数据类型的属性是,JavaScript会自动调用其构造函数来生成一个对象。例如:
(3).constructor === Number //true
true.constructor === Boolean //true
"aaa".constructor === String //ture
//相当于
(new Number(3)).constructor === Number
(new Boolean(true)).constructor === Boolean
(new String("aaa")).constructor === String
- 跨窗口问题
JavaScript是运行在宿主环境下的,而每个宿主环境会提供一套ECMA标准的内置对象以及宿主对象(如widow,document),一个新的窗口即是一个新的宿主环境。不同窗口下的内置对象是不同的实例,拥有不同的内存地址。
// a.html
<script>
var a = [1,2,3];
</script>
// main.html
<iframe src="a.html"></iframe>
<script>
var frame = window.frames[0];
var a = frame.a;
console.log(a instanceof Array); // false
console.log(a.contructor === Array); //false
console.log(a instanceof frame.Array); // true
</script>
而instanceof和constructor都是通过比较两个Function是否相等来进行判断的。此时很显然会出现问题的
var iframe=document.createElement("iframe");
var iWindow=iframe.contentWindow;
document.body.appendChild(iframe);
iWindow.Array === Array //false
//相当于
iWindow.Array === window.Array //false
因此iWindow中的数组arr原型链上是没有window.Array的。
iWindow.document.write('<script> var arr = [1, 2]</script>');
iWindow.arr instanceof Array // false
iWindow.arr instanceof iWindow.Array // true
- toString
toString 方法是最为可靠的类型检测手段,它会将当前对象转换为字符串并输出。toString 属性定义在Object.prototype
上,因而所有对象都拥有toString 方法。但Array、Date等对象会重写从Object.prototype继承来的toString,所以最好用Object.prototype.toString来检测类型。
每个类在内部都有一个 [[Class]] 属性,这个属性中就指定了上述字符串中的构造函数名,Object.prototype.toString 的原理是当调用的时候, 就取值内部的 [[Class]] 属性值, 然后拼接成 ‘[object ‘ + [[Class]] + ‘]’ 这样的字符串并返回. 然后我们使用 call 方法来获取任何值的数据类型。
toString=Object.prototype.toString;
toString.call(new Date);//[object Date]
toString.call(new String);//[object String]
toString.call(Math);//[object Math]
toString.call(3);//[object Number]
toString.call([]);//[object Array]
toString.call({});//[object Object]
//Since JavaScript 1.8.5
toString.call(undefined);//[object Undefined]
toString.call(unll);//[object Unll]
toString 也不是完美的,它无法检测用户自定义类型。因为Object.prototype是不知道用户会创造什么类型的,它只能检测ECMA标准中的拿些内置类型。
toString.call(new Animal)//[object Object]
因为返回值是字符串,也避免了跨窗口问题。当然IE弹窗中还是有bug,那就不需要管它,现在还有多少人在用IE?多少人还在用弹框?
和Object.prototype.toString类似,Function.prototype.toString也有类似功能,不过它的this只能是Function,其他类型(例如基本数据类型)都会抛出异常。
小小总结
typeof只能检测基本数据类型,对于null会存在bug
- instanceof适用于检测对象,但是基于原型链运作的
- constructor指向的是最初创建者,而且容易伪造,不适合做类型判断
- toString适用于ECMA内置JavaScript类型(包括基本数据类型和内置对象)的类型判断
- 基于引用类型等的类型检查都有跨窗口问题,比如instanceof和constructor
总之,如果要判断的是基本数据类型或JavaScript内置对象,使用toString;如果要判断的是自定义类型,可以使用instanceof。
18. 谈谈对JSON的了解
JSON是什么?
JSON(JavaScript Object Notation)是一个轻量级的数据交换格式,容易与人阅读和编写,同时易于机器解析和生成,JSON采用完全独立语言的文本格式,而且很多语言都提供了对JSON的支持(包括c、c++、java、c#等),这样就会使得JSON成为理想的数据交换格式。
轻量级指的是和xml作比较,数据交换指的是客户端和服务器之间业务数据的传递格式。
JSON在JavaScript中的应用
- JSON的定义
JSON是由键值对组成,并且是由花括号(大括号)包围。每个键由引号引起来,键和值之间使用冒号进行分割,多组键值对之间使用逗号进行分割
var perosnObj = {
"name" : "sunny",
"age" : 11,
"hobby" : "swimming",
"address" : ["beijing","shanghai","sichuang"],
"key_1" : {
"key_1_1" : 551,
"key_1_2" : "key_1_2_value"
},
"key_2" : [{
"key_2_1_1" : 6611,
"key_2_1_2" : "key_2_2_2_value"
}]
}
- JSON的访问
JSON本身是一个对象
JSON中的key可以理解为是对象中的一个属性
JSON中的key访问就跟访问对象的属性一样:JSON对象.key
- JSON中的两个常用的方法
JSON的存在有两种方式
一种是对象的形式存在,我们叫它JSON对象
一种是字符串的形式存在,我们叫它JSON字符串
一般我们要操作json中的数据的时候,需要json对象的格式
一般我们要在客户端和服务器之间进行数据交换的时候,使用json字符串
JSON.stringify() 把json对象转化为json字符串 JSON.parse() 把json字符串转换为json对象
19. 进行哪些操作会造成内存泄漏?
什么是内存泄漏?内存泄漏
是指由于疏忽或错误造成程序未能释放已经不在使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。内存泄漏通常情况下只能由获得程序源代码的程序员才能分析出来。然而,有不少人习惯把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。(内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果)
常见的内存泄漏
- 意外的全局变量
JavaScript对未声明变量的处理方式:在全局对象上创建该变量的引用,(即全局对象上的属性,不是变量,因为它能通过 delete
删除)。如果在浏览器中,全局对象就是window对象。如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才会被释放,这样会造成意外的内存泄漏。
说到全局变量,需要注意那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。全局变量也常用来做cache,一般cache都是为了性能优化才用到的,为了性能,最好对cache的大小做个上限限制。因为cache是不被回收的,越高cache会导致越高的内存消耗。
- 闭包
由于闭包会携带包含它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多
原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象。如果创建的内部函数没有被其他对象引用引用,不管内部函数是否引用外部函数的变量和参数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或参数的访问,并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包。外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。
- DOM泄漏
在JavaScript中,DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。比如Chrome浏览器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两座孤岛之间通过一座收费桥连接,过桥需要缴纳一定的”过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要缴纳”过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大的影响。为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行 某些删除、更新操作时
,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄漏。
- 定时器
在JavaScript常用 setInterval()、setTimeout()链式调用
来实现一些动画效果。如果我们不需要 setInterval()
时,没有通过 clearInterval()
方法移除,那么 setInterval()
会不停的调用函数,直到调用 clearInterval()
或者 关闭窗口
。如果链式 setTimeout()
调用模式没有给出终止逻辑,也会一直运行下去。因此不需要重复定时器时,确保对定时器进行清除,避免占用资源。另外,在使用 setInterval()
和 setTimeout()
来实现动画时,无法确保定时器按照指定的时间间隔来执行动画。
- 事件函数(EventListener)
项目中的一个场景:做移动开发时,需要对不同设备尺寸做适配。如在开发组件时,有时需要考虑处理横竖屏适配问题。一般做法,在横竖屏发生变化时,需要将组件销毁后再重新生成。而在组件中会对其进行相关事件绑定,如果在销毁组件时,没有将组件的事件解绑,在横竖屏发生变化时,就会不断地对组件进行事件绑定。这样会导致一些异常,甚至可能会导致页面崩掉。同一个元素节点注册了多个相同的EventListener,那么重复的实例会被抛弃。这么做不会让EventListener被重复调用,也不需要用removeEventListener手动清除多余的EventListener,因为重复的都被自动抛弃了。而这条规则只是针对于命名函数。
20. 谈谈你所理解的函数式编程。
什么是函数式编程?
在计算机科学中,函数式编程是一种编程范式—-构建计算机程序结构和元素的一种风格—-它把计算当做数学上的函数对待,避免状态的改变以及可变数据。它是一种声明式的编程范式,因为程序是用表达式或者声明而不是使用语句来完成的。在函数中,输出的值只依赖域它的参数,换句话说相同的输入总得到相同的输出。这与命令式编程相反,在命令式编程中,除了函数参数,全局程序装填也可以影响函数最终的结果。消除副作用,既不依赖函数输出的状态变化,这能使程序更容易理解,这也是使用函数式编程开发程序的主要动机之一。
换个大白话来说就是:根据学术上函数的定义,函数是一种描述集合和集合之间的转换关系,输入通过函数都会返回有且只有一个输出值,所以函数实际上是一个关系,或者说是一种映射,而这种映射关系是可以组合的,一旦我们知道一个函数的输出类型可以匹配另一个函数的输出,那他们可以进行组合。在我们的编程世界里,我们需要处理的其实只有”数据”和”关系”,而关系就是函数。我们所谓的编程工作也不过就是在找一种 映射关系
,一旦关系找到啦,问题就解决啦,剩下的事情,就是让数据流过这种关系,然后转换成另一个数据罢了。或者是用 流水线
来形容,把输入当作原材料,把输出当做产品, 数据可以不断从一个函数的输出可以流入另一个函数输入
,最后在输出结果,这不正是一套流水线么?所以函数式编程更多强调在编写过程中把更多的关注点放在如何构建关系
。通过构建一条高效的流水线工程,一次性解决所有问题,而不是把精力分散在不同的加工厂中来回奔波传递数据。
函数式编程的特点:
- 函数是”一等公民”
这是函数式编程实现的 前提
,因为我们基本的操作都是操作函数。这个特性意味着函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。
- 声明式编程
函数式编程大多时候都是在声明我需要什么,而不是怎么去做。这种编程风格称为声明式编程。这个有个好处是:代码的可读性特别高,因为声明式代码大多都是接近自然语言,同时,它解放了大量的人力,因为他不关心具体的实现,因此它可以把优化能力交给具体的实现,这也方便我们进行分工协作。
SQL 语句就是声明式的,不需要关心Select语句是怎样实现的,不同的数据库会去实现它自己的方法并且优化。React也是声明式的,只要描述自己的UI,接下来状态变化后UI如何更新,是React在运行时帮你处理的,而不是靠自己去渲染和优化diff算法。
- 惰性执行
所谓的惰性执行指的是函数只在需要的时候执行,既不产生无意义的中间变量。函数式编程和命令式编程最大的区别就是在于几乎没有中间变量,从头到尾都在写函数,只有在最后的时候才通过调用函数产生实际(最终)的结果。
- 无状态和数据不可变
这个是函数式编程的核心概念
数据不可变:它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
无状态:主要强调对于一个函数,不管你何时运行,都和第一次运行时一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化
为了实现这个目标,函数式编程提出函数应该具备的特性:没有副作用和纯函数
没有副作用:它的含义是:在完成函数主要功能之外完成的其他副要功能。在我们函数中最主要的功能当然是根据输入返回结果,而在函数中我们最常见的副作用是随意操纵外部变量。由于JS中对象传递的是引用地址,哪怕我们用const官架子声明对象,它依旧是可变的,正是这个'漏洞'让我们有机会可以随意修改对象。保证函数没有副作用,一来能保证数据的不可变性,二来能避免很多因共享状态带来的问题。当你一个人维护代码的时候可能不明显,但是随着项目的迭代,项目参与人数增加,大家对同一变量的依赖和引用越来越多,这种问题会越来越严重。最终可能连维护者自己都不清楚变量到底是在哪儿被改变而产生的bug
纯函数:传函数的概念很简单,就是两点:不依赖外部状态(无状态,函数的运行结果不依赖全局变量,比如this指针,IO操作等)和没有副作用(数据不变,不修改全局变量,不修改入参),纯函数才是真正意义上的"函数",意味着`相同的输入,永远得到相同的输出`.
强调纯函数的意义:
- 便于测试和优化:这个意义在实际项目开发中意义非常大,由于纯函数对于相同的输入永远返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行,这十分符合测试驱动开发TDD的思想,这样产生的代码往往健壮性更强。
- 可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的结果。比如有很多库会有所谓的memoize函数,可以缓存函数的结果。
- 自文档化:由于纯函数没有副作用,所以其依赖很明确,因此更易于观察和理解
- 更少的bug:使用纯函数意味着你的函数中不存在指向不明的this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数bug的源头