Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端基础技能之JavaScript #35

Open
wolfdu opened this issue Mar 7, 2018 · 1 comment
Open

前端基础技能之JavaScript #35

wolfdu opened this issue Mar 7, 2018 · 1 comment

Comments

@wolfdu
Copy link
Owner

wolfdu commented Mar 7, 2018

最近正在准备找工作,作为野路子前端个人认为很有必要梳理一下前端相关基础知识(刷题),所以分三大块HTML、CSS和JavaScript来进行整理记录。
其中很多知识点并不是一两句话就能聊清楚的,所以很多内容都是粗鄙浅显的,更加深入的讨论需要单独开坑,所以引入了其他大牛整理好的相关内容以供翻阅参考。
切记不可急于求成,不能妄自菲薄。

JavaScript原型,原型链 ? 有什么特点?

原型链

  • 构造函数的prototype属性指向的是由构造函数创建(new)的实例的原型
  • 实例的__proto__属性指向的是该实例的原型对象
  • 原型对象的constructor属性指向关联的构造函数
  • 在当前实例中查找属性如果没有找到,会顺着原型链一直向上查找(类似作用域的查找),直到查找到顶端为止
  • Object.prototype没有原型对象(为null表示木有)。

JavaScript从原型到原型链

JavaScript有几种类型的值?,你能画一下他们的内存图吗?

栈:原始数据类型(Undefined,Null,Boolean,Number、String)
堆:引用数据类型(对象、数组和函数)

// 结合下面的内存空间图观察
var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中

对应内存空间

两种类型的区别是:存储位置不同。
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
javascript内存空间详解

JavaScript实现乱序

var values = [1, 2, 3, 4, 5];

values.sort(function(){
    return Math.random() - 0.5;
});

console.log(values)

使用Math.random乱的不够彻底,可以使用Fisher–Yates算法进行乱序:

function shuffle(a) {
    for (let i = a.length; i; i--) {
        let j = Math.floor(Math.random() * i);
        [a[i - 1], a[j]] = [a[j], a[i - 1]];
    }
    return a;
}

JavaScript专题之乱序

javascript创建对象的几种方式

  • 工厂模式
    缺点:对象无法识别,因为所有的实例都指向一个原型
function createPerson(name) {
    var o = new Object();
    o.name = name;
    o.getName = function () {
        console.log(this.name);
    };
    return o;
}
var person1 = createPerson('kevin');
  • 构造函数模式
    优点:实例可以识别为一个特定的类型
    缺点:每次创建实例时,每个方法都要被创建一次
function Person(name) {
    this.name = name;
    this.getName = function () {
        console.log(this.name);
    };
}
var person1 = new Person('kevin');
  • 原型模式
    优点:方法不会重新创建
    缺点:
    1.所有的属性和方法都共享
    2.不能初始化参数
function Person() {
}
Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
    console.log(this.name);
};
var person1 = new Person();
  • 组合模式:构造函数模式与原型模式双剑合璧
    优点:该共享的共享,该私有的私有,使用最广泛的方式
    缺点:有的人就是希望全部都写在一起,即更好的封装性
function Person(name) {
    this.name = name;
}
Person.prototype = {
    constructor: Person,
    getName: function () {
        console.log(this.name);
    }
};
var person1 = new Person();

JavaScript深入之创建对象的多种方式以及优缺点

Javascript如何实现继承

  • 原型链继承:
    缺点:
    1.引用类型的属性被所有实例共享
    2.在创建 Child 的实例时,不能向Parent传参
function Parent () {
    this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
  • 借用构造函数(经典继承):
    优点:
    1.避免了引用类型的属性被所有实例共享
    2.可以在 Child 中向 Parent 传参
    缺点:
    方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Parent () {
    this.names = ['kevin', 'daisy'];
}
function Child () {
    Parent.call(this);
}
  • 组合继承:原型链继承和经典继承双剑合璧。
    优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
    console.log(this.name)
}
function Child (name, age) {

    Parent.call(this, name);
    
    this.age = age;
}
Child.prototype = new Parent();
  • 原型式继承:就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
    缺点:
    包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}
  • 寄生式继承
    缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法
function createObj (o) {
    var clone = object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}
  • 寄生组合式继承
    引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:
    这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);

JavaScript深入之继承的多种方式和优缺点

Javascript作用域链

函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。

其中作用域链,在解释器进入到一个执行上下文时,初始化完成并将其分配给当前执行上下文。每个执行上下文的作用域链由当前上下文的变量对象(VO)及父级执行上下文的作用域链构成。

可以理解为:currentScopeChain = currentVO + all parents scopes

JavaScript执行上下文

谈谈this对象的理解

this的指向,是在函数被调用的时候确定的,也就是在创建EC阶段
涉及到this指向问题需要考虑到如下情况:全局下,函数独立调用,对象方法调用,new,call,apply,bind。

关于this的详细解析-->this到底指向哪里

undefined与null的区别

null表示"没有对象",即该处不应该有值。

  1. 作为函数的参数,表示该函数的参数不是对象。
  2. 作为对象原型链的终点。

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

  1. 变量被声明了,但没有赋值时,就等于undefined。
  2. 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  3. 对象没有赋值的属性,该属性的值为undefined。
  4. 函数没有返回值时,默认返回undefined。

所以设置一个值为 null 是合理的,如:
objA.valueA = null;
但设置一个值为 undefined 是不合理的

undefined与null的区别

["1", "2", "3"].map(parseInt) 答案是多少?

parseInt函数解析一个字符串参数,并返回一个指定基数的整数。

parseInt(string, radix);

radix
一个介于2和36之间的整数(数学系统的基础),表示上述字符串的基数。比如参数"10"表示使用我们通常使用的十进制数值系统。始终指定此参数可以消除阅读该代码时的困惑并且保证转换结果可预测。当未指定基数时,不同的实现会产生不同的结果,通常将值默认为10。

返回解析后的整数值。 如果被解析参数的第一个字符无法被转化成数值类型,则返回 NaN。

Array.prototype.map()方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

let new_array = arr.map(function callback(currentValue, index, array) { 
    // Return element for new_array 
}[, thisArg])

可以发现["1", "2", "3"].map(parseInt)实际执行为:

parseInt('1', 0); // OK - gives 1
parseInt('2', 1); // FAIL - 1 isn't a legal radix
parseInt('3', 2); // FAIL - 3 isn't legal in base 2 

那么最终结果为[1,NaN,NaN]

Why does parseInt yield NaN with Array#map?

什么是闭包(closure),为什么要用它?

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

ECMAScript中,闭包指的是:

  • 从理论角度: 所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:
    1.即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2.在代码中引用了自由变量

JavaScript深入之闭包

对象的类型判断

  • typeof操作符返回一个字符串,指示未经计算的操作数的类型。
  • Object.prototype.toString()方法返回一个表示该对象的字符串。
  • Object.prototype.constructor返回创建实例对象的 Object 构造函数的引用。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。
  • instanceof运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

jQuery中Type API:

var class2type = {};

// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
    class2type["[object " + item + "]"] = item.toLowerCase();
})

function type(obj) {
    // 一箭双雕
    if (obj == null) {
        return obj + "";
    }
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[Object.prototype.toString.call(obj)] || "object" :
        typeof obj;
}

JavaScript专题之类型判断(上)
JavaScript专题之类型判断(下)

new操作符具体干了什么

放出一个模拟板:

function objFactory(){
    // 初始化一个临时对象
    var obj = new Object()
    // 获取第一个参数为构造函数,shift会改变原数组哦
    var Construct = [].shift.call(arguments)
    // 将临时对象的原型指向构造函数的prototype
    obj.__proto__ = Construct.prototype
    // 使用apply给obj添加属性
    var result = Construct.apply(obj, arguments)

    return (typeof result === 'object' && result !== null) ? result : obj
}
  • 使用new运算符的时候一个新对象被创建,它的原型对象指向构造函数的prototype
  • 使用new运算符构造函数中的this会指向一个新创建的对象
  • 构造函数和普通函数并没有什么两样,关键在于你是否使用new运算符

new运算符模拟实现

===运算符判断相等的流程是怎样的

  1. 如果两个值不是相同类型,它们不相等
  2. 如果两个值都是null或者都是undefined,它们相等
  3. 如果两个值都是布尔类型true或者都是false,它们相等
  4. 如果其中有一个是NaN,它们不相等
  5. 如果都是数值型并且数值相等,他们相等, -0等于0
  6. 如果他们都是字符串并且在相同位置包含相同的16位值,他它们相等;如果在长度或者内容上不等,它们不相等;两个字符串表示结果相同但是编码不同==和===都认为他们不相等
  7. 如果他们指向相同对象、数组、函数,它们相等;如果指向不同对象,他们不相等

==运算符判断相等的流程是怎样的

  1. 如果两个值类型相同,按照===比较方法进行比较
  2. 如果类型不同,使用如下规则进行比较
  3. 如果其中一个值是null,另一个是undefined,它们相等
  4. 如果一个值是数字另一个是字符串,将字符串转换为数字进行比较
  5. 如果有布尔类型,将true转换为1,false转换为0,然后用==规则继续比较
  6. 如果一个值是对象,另一个是数字或字符串,将对象转换为原始值然后用==规则继续比较
  7. 其他所有情况都认为不相等

函数内部arguments变量有哪些特性,有哪些属性,如何将它转换为数组

  1. arguments所有函数中都包含的一个局部变量,是一个类数组对象,对应函数调用时的实参。如果函数定义同名参数会在调用时覆盖默认对象
  2. arguments[index]分别对应函数调用时的实参,并且通过arguments修改实参时会同时修改实参
  3. arguments.length为实参的个数(Function.length表示形参长度)
  4. arguments.callee为当前正在执行的函数本身,使用这个属性进行递归调用时需注意this的变化
  5. arguments.caller为调用当前函数的函数(已被遗弃)
  6. 转换为数组:var args = Array.prototype.slice.call(arguments, 0);

DOM操作 —— 如何添加、移除、移动、复制、创建和查找节点等。

添加

Node.appendChild()方法将一个节点添加到指定父节点的子节点列表末尾。

移除

ChildNode.remove() 方法把从它所属的DOM树中删除对象。

移动

Node.replaceChild()用指定的节点替换当前节点的一个子节点,并返回被替换掉的节点。

复制

Node.cloneNode()方法返回调用该方法的节点的一个副本。

创建

Document.createDocumentFragment() 创建一个新的空白的文档片段。
Document.createElement() 创建由tagName 指定的HTML元素,或一个HTMLUnknownElement,如果tagName不被识别。
document.createTextNode() 创建一个新的文本节点。

查找节点

Element.querySelector()返回与指定的选择器组匹配的元素的后代的第一个元素。
Element.querySelectorAll()返回一个non-live的NodeList, 它包含所有元素的非活动节点,该元素来自与其匹配指定的CSS选择器组的元素。(基础元素本身不包括,即使它匹配。)

document.getElementById()返回一个匹配特定 ID的元素.

Element.getElementsByClassName()Element.getElementsByClassName() 方法返回一个即时更新的(live) HTMLCollection,包含了所有拥有指定 class 的子元素。当在 document 对象上调用此方法时,会检索整个文档,包括根元素。

element.getElementsByTagName()方法返回一个动态的包含所有指定标签名的元素的HTML集合HTMLCollection。指定的元素的子树会被搜索,不包括元素自己。返回的列表是动态的,这意味着它会随着DOM树的变化自动更新自身。所以,使用相同元素和相同参数时,没有必要多次的调用Element.getElementsByTagName() .

事件 —— 如何使用事件,以及IE和标准DOM事件模型之间存在的差别。

addEventListener:绑定事件的监听函数
removeEventListener:移除事件的监听函数
dispatchEvent:触发事件

DOM提供三种方法,可以用来为事件绑定监听函数。

  1. HTML标签的on-属性
  2. Element节点的事件属性
  3. addEventListener方法

事件模型

DOM事件的绑定的几种方式

  1. 在DOM元素中直接绑定;
  2. 在JavaScript代码中绑定;
  3. 绑定事件监听函数。

XMLHttpRequest —— 这是什么、怎样完整地执行一次GET请求、怎样检测错误。

  1. 创建XMLHttpRequest对象,也就是创建一个异步调用对象
  2. 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息
  3. 设置响应HTTP请求状态变化的函数
  4. 发送HTTP请求
  5. 获取异步调用返回的数据
  6. 使用JavaScript和DOM实现局部刷新
var xhr = new XMLHttpRequest();

// 指定通信过程中状态改变时的回调函数
xhr.onreadystatechange = function(){
  // 通信成功时,状态值为4
  if (xhr.readyState === 4){
    if (xhr.status === 200){
      console.log(xhr.responseText);
    } else {
      console.error(xhr.statusText);
    }
  }
};

xhr.onerror = function (e) {
  console.error(xhr.statusText);
};

// open方式用于指定HTTP动词、请求的网址、是否异步
xhr.open('GET', '/endpoint', true);

// 发送HTTP请求
xhr.send(null);

AJAX

什么是window对象

window对象在浏览器中,window对象(注意,w为小写)指当前的浏览器窗口。它也是所有对象的顶层对象

什么是document对象

document节点是文档的根节点,每张网页都有自己的document节点。window.document属性就指向这个节点。只要浏览器开始载入HTML文档,这个节点对象就存在了,可以直接调用。

cookies,sessionStorage 和 localStorage 的区别?

cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)。
cookie数据始终在同源的http请求中携带(即使不需要),即会在浏览器和服务器间来回传递。
sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。

存储大小:

  • cookie数据大小不能超过4k。
  • sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。

有期时间:

  •   localStorage    存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;
    
  •   sessionStorage  数据在当前浏览器窗口关闭后自动删除。
    
  •   cookie          设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
    

共享:

  • sessionStorage不能共享,localStorage在同源文档之间共享,cookie在同源且符合path规则的文档之间共享

localStorage的修改会促发其他文档窗口的update事件

通用的事件侦听器函数

// event(事件)工具集,来源:https://github.com/markyun/Markyun.js
markyun.Event = {
	// 页面加载完成后
	readyEvent : function(fn) {
		if (fn==null) {
			fn=document;
		}
		var oldonload = window.onload;
		if (typeof window.onload != 'function') {
			window.onload = fn;
		} else {
			window.onload = function() {
				oldonload();
				fn();
			};
		}
	},
	// 视能力分别使用dom0||dom2||IE方式 来绑定事件
	// 参数: 操作的元素,事件名称 ,事件处理程序
	addEvent : function(element, type, handler) {
		if (element.addEventListener) {
			//事件类型、需要执行的函数、是否捕捉
			element.addEventListener(type, handler, false);
		} else if (element.attachEvent) {
			element.attachEvent('on' + type, function() {
				handler.call(element);
			});
		} else {
			element['on' + type] = handler;
		}
	},
	// 移除事件
	removeEvent : function(element, type, handler) {
		if (element.removeEventListener) {
			element.removeEventListener(type, handler, false);
		} else if (element.detachEvent) {
			element.detachEvent('on' + type, handler);
		} else {
			element['on' + type] = null;
		}
	},
	// 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
	stopPropagation : function(ev) {
		if (ev.stopPropagation) {
			ev.stopPropagation();
		} else {
			ev.cancelBubble = true;
		}
	},
	// 取消事件的默认行为
	preventDefault : function(event) {
		if (event.preventDefault) {
			event.preventDefault();
		} else {
			event.returnValue = false;
		}
	},
	// 获取事件目标
	getTarget : function(event) {
		return event.target || event.srcElement;
	},
	// 获取event对象的引用,取到事件的所有信息,确保随时能使用event;
	getEvent : function(e) {
		var ev = e || window.event;
		if (!ev) {
			var c = this.getEvent.caller;
			while (c) {
				ev = c.arguments[0];
				if (ev && Event == ev.constructor) {
					break;
				}
				c = c.caller;
			}
		}
		return ev;
	}
};

target和currentTarget的区别

Event.target一个触发事件的对象的引用。它与event.currentTarget不同, 当事件处理程序在事件的冒泡或捕获阶段被调用时。

event.currentTarget当事件遍历DOM时,标识事件的当前目标。它总是引用事件处理程序附加到的元素,而不是event.target,它标识事件发生的元素。

  • e.target 指向触发事件监听的对象。
  • e.currentTarget 指向添加监听事件的对象。

e.target与e.currentTarget的区别

模块化开发

JS模块化方案很多,AMD/CommonJS/UMD/ES6 Module等。

由于nodejs流行所以现在CommonJS规范使用的人数较多,当然ES6 Module应该会慢慢成为主流,至于AMD和CMD由于seajs和requirejs使用越来越少所以这两个规范的应用应该也是一样吧。

实现一个 CMD 模块加载器

实现一个 CMD 模块加载器轮子

  1. 首先,通过 use 方法来加载入口模块,并接收一个回调函数, 当模块加载完成, 会调用回调函数,并传入对应的模块。use 方法会 check 模块有没有缓存,如果有,则从缓存中获取模块,如果没有,则创建并加载模块。
  2. 获取到模块后,模块可能还没有 load 完成,所以需要在模块上绑定一个 “complete” 事件,模块加载完成会触发这个事件,这时候才调用回调函数。
  3. 创建一个模块时,id就是模块的地址,通过创建 script 标签的方式异步加载模块的代码(factory),factory 加载完成后,会 check factory 中有没有 require 别的子模块:
    3.1 如果有,继续加载其子模块,并在子模块上绑定 “complete” 事件,来触发本身 的 “complete” 事件;
    3.2 如果没有则直接触发本身的 “complete” 事件。
    3.3 如果子模块中还有依赖,则会递归这个过程。
  4. 通过事件由里到外的传递,当所有依赖的模块都 complete 的时候,最外层的入口模块才会触发 “complete” 事件,use 方法中的回调函数才会被调用

如何实现一个 CMD 模块加载器
前端模块化、组件化

ECMAScript6 为什么会出现class这种东西?

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

Class 的基本语法

.call() 和 .apply() 的区别

我们可以知道call()方法的作用和 apply() 方法类似,只有一个区别,就是 call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组。

JavaScript模拟实现call,apply,bind等方法

数组和对象有哪些原生方法

Object构造函数创建一个对象包装器。

  • hasOwnProperty()
  • isPrototypeOf()
  • toString()

Array用于构造数组的全局对象

  • forEach
  • push
  • pop
  • shift
  • unshift

Array对象

JavaScript中的作用域与变量声明提升

创建变量对象过程:

  1. 创建arguments object,检查当前上下文参数,初始化参数名称和值,并创建一个参考副本(全局环境下没有该过程)。
  2. 检查当前上下文函数声明(使用function声明的函数):
    • 在VO中创建一个对应函数名的属性其值指向该函数所在内存地址的引用
    • 如果VO中函数名已经存在,则覆盖原有的引用
  3. 检查当前上下文变量声明:
    • 在VO中创建一个对应变量名的属性,并将该值初始化为undefined
    • 如果VO中变量名已经存在,则什么也不做,原属性值不会被修改

从创建变量对象步骤1,,2,3可以发现这就是我们常说的变量提升和函数声明优先级高于变量声明的原因。

JavaScript执行上下文

如何实现深拷贝?

数组的浅拷贝

利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。

var arr = ['old', 1, true, null, undefined];

var new_arr1 = arr.concat();

var new_arr2 = arr.slice();

手动实现一个浅拷贝:

var shallowCopy = function(obj) {
    // 只拷贝对象
    if (typeof obj !== 'object') return;
    // 根据obj的类型判断是新建一个数组还是对象
    var newObj = obj instanceof Array ? [] : {};
    // 遍历obj,并且判断是obj的属性才拷贝
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}

我们把这种复制引用的拷贝方法称之为浅拷贝,与之对应的就是深拷贝,深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。

数组的深拷贝

var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]

var new_arr = JSON.parse( JSON.stringify(arr) );

这是一个简单粗暴的好方法,就是有一个问题,不能拷贝函数。

手动实现一个深拷贝:

var deepCopy = function(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}

尽管使用深拷贝会完全的克隆一个新对象,不会产生副作用,但是深拷贝因为使用递归,性能会不如浅拷贝,在开发中,还是要根据实际情况进行选择。

JavaScript专题之深浅拷贝

jQuery 的属性拷贝(extend)的实现原理是什么?

extend 的用法:
jQuery.extend( [deep], target, object1 [, objectN ] )

函数的第一个参数可以传一个布尔值,如果为 true,我们就会进行深拷贝,false 依然当做浅拷贝。 target,表示要拓展的目标,我们就称它为目标对象吧。
后面的参数,都传入对象,内容都会复制到目标对象中,我们就称它们为待复制对象吧。

实现思路:

  1. 需要根据第一个参数的类型,确定 target 和要合并的对象的下标起始值。
  2. 如果是深拷贝,根据 copy 的类型递归 extend。
// 第二版
function extend() {
    // 默认不进行深拷贝
    var deep = false;
    var name, options, src, copy;
    var length = arguments.length;
    // 记录要复制的对象的下标
    var i = 1;
    // 第一个参数不传布尔值的情况下,target默认是第一个参数
    var target = arguments[0] || {};
    // 如果第一个参数是布尔值,第二个参数是才是target
    if (typeof target == 'boolean') {
        deep = target;
        target = arguments[i] || {};
        i++;
    }
    // 如果target不是对象,我们是无法进行复制的,所以设为{}
    if (typeof target !== 'object') {
        target = {}
    }

    // 循环遍历要复制的对象们
    for (; i < length; i++) {
        // 获取当前对象
        options = arguments[i];
        // 要求不能为空 避免extend(a,,b)这种情况
        if (options != null) {
            for (name in options) {
                // 目标属性值
                src = target[name];
                // 要复制的对象的属性值
                copy = options[name];

                if (deep && copy && typeof copy == 'object') {
                    // 递归调用
                    target[name] = extend(deep, src, copy);
                }
                else if (copy !== undefined){
                    target[name] = copy;
                }
            }
        }
    }

    return target;
};

核心的部分还是跟实现深浅拷贝函数一致,如果要复制的对象的属性值是一个对象,就递归调用 extend。不过 extend 的实现中,多了很多细节上的判断,比如第一个参数是否是布尔值,target 是否是一个对象,不传参数时的默认值等。

以上实现和jQuery中版本还有一定差别,详细内容-->JavaScript专题之从零实现jQuery的extend

谈一下Jquery中的bind(),live(),delegate(),on()的区别?

bind()

.bind()方法将事件类型和一个事件处理函数直接注册到了被选中的DOM元素中。

live()

.live()方法将与事件处理函数关联的选择器和事件信息一起附加到文档的根级元素(即document)。通过将事件信息注册到document上,这个事件处理函数将允许所有冒泡到document的事件调用它(例如委托型、传播型事件)。一旦有一个事件冒泡到document元素上,Jquery会根据选择器或者事件的元数据来决定哪一个事件处理函数应该被调用,如果这个事件处理函数存在的话。这个额外的工作将会在用户交互时对性能方面造成一定的影响,但是初始化注册事件的过程相当地快。

delegate()

.delegate()方法与.live()方式实现方式相类似,它不是将选择器或者事件信息附加到document,而是让你指定附加的元素。就像是.live()方法一样,这个方法使用事件委托来正确地工作。

on()

Jquery 1.7版本中.bind(),.live() 和.delegate()方法只需要使用.on()方法一种方式来调用它们。
如何使用.on()方法决定了它如何调用其他方法。你可以认为.on()方法被具有不同签名的方法”重载“了,而这些方法实现了不同的事件绑定的连接方式。

/* Jquery的 .bind() , .live() 和 .delegate() 方法只需要使用`.on()`方法一种方式来调用它们 */

// Bind
$( "#members li a" ).on( "click", function( e ) {} ); 
$( "#members li a" ).bind( "click", function( e ) {} ); 

// Live
$( document ).on( "click", "#members li a", function( e ) {} ); 
$( "#members li a" ).live( "click", function( e ) {} );

// Delegate
$( "#members" ).on( "click", "li a", function( e ) {} ); 
$( "#members" ).delegate( "li a", "click", function( e ) {} );

[译] Jquery中 .bind() .live() .delegate() 和 .on() 之间的区别

JS之事件节流

节流的原理很简单:
如果你持续触发事件,每隔一段时间,只执行一次事件。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

JavaScript专题之跟着 underscore 学节流

JS的函数防抖

简单解释一下debounce的原理:
在持续触发某个事件时,该事件的回调函数不会立即触发,而是在最后一个触发事件结束一段时间后去执行回调函数。

我们会为目标函数设置一个定时器,当定时器未执行时,事件再次被触发,则清除当前的定时器,然后设置新的定时器,直到定时器timeout执行目标函数。

function debounceTwo(fun, wait){
    var timeout;
    return function(){
        // 获取当前this,用于指定目标函数的this值
        var context = this;
        // 获取调用时的参数
        var args = arguments;

        clearTimeout(timeout);
        timeout = setTimeout(function(){
            fun.apply(context, args);
        }, wait);
    }
};

_.debounce源码学习
JavaScript专题之跟着underscore学防抖

Babel了解?

Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。

如下是一个简单的babel配置文件.babelrc

{
	// 设置转码规则,使用的时候需要安装对应的插件,
	// 对应babel-preset-xxx,例如下面的配置babel-preset-env
	"presets": [
		["env", {// options
			"modules": false,// 不会转换模块
			"targets": {
				"browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 
				// 使用BrowserList(https://github.com/ai/browserslist)表查询选择浏览器
			}
		}],
		"stage-2"
	],
	// 插件,该插件解决编译中产生的重复的helper函数,减小代码体积
	"plugins": ["transform-runtime"],
	"env": {// 特定环境下执行编译规则
		"test": {// env为test时的编译规则
			"presets": ["env", "stage-2"],// 同上
			"plugins": ["istanbul"]// 测试使用插件
		}
	}
}

stage-x

stage-x和es2015等有些类似,但是它是按照JavaScript的提案阶段区分的,一共有5个阶段。
而数字越小,阶段越靠后,存在依赖关系。也就是说stage-0是包括stage-1的以此类推。

transform-runtime

Runtime transform主要有以下options选择。

helpers: boolean,默认true
使用babel的helper函数。

polyfill: boolean,默认true
使用babel的polyfill,但是不能完全取代babel-polyfill。

regenerator: boolean,默认true
使用babel的regenerator。

moduleName: string,默认babel-runtime
使用对应module处理。

这里的options一般不用自己设置,用默认的即可。这个插件最大的作用主要有几下几点:

  • 解决编译中产生的重复的工具函数,减小代码体积
  • 非实例方法的poly-fill,如Object.assign,但是实例方法不支持,如"foobar".includes("foo"),这时候需要单独引入babel-polyfill

babel-polyfill

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。
举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

如何写好.babelrc?
Babel 入门教程

JSONP 的工作原理是什么?

利用<script>标签没有跨域限制的“漏洞”(历史遗迹啊)来达到与第三方通讯的目的。当需要通讯时,本站脚本创建一个<script>元素,地址指向第三方的API网址,形如: <script src="http://www.example.net/api?param1=1&param2=2"></script> 并提供一个回调函数来接收数据(函数名可约定,或通过地址参数传递)。 第三方产生的响应为json数据的包装(故称之为jsonp,即json padding),形如: callback({"name":"hax","gender":"Male"}) 这样浏览器会调用callback函数,并传递解析后json对象作为参数。本站脚本可在callback函数里处理所传入的数据。
补充:“历史遗迹”的意思就是,如果在今天重新设计的话,也许就不会允许这样简单的跨域了嘿,比如可能像XHR一样按照CORS规范要求服务器发送特定的http头。

贺师俊知乎回答
说说JSON和JSONP,也许你会豁然开朗,含jQuery用例

CORS通信

同源:两个文档同源需满足

  • 协议相同
  • 域名相同
  • 端口相同

跨域通信:js进行DOM操作、通信时如果目标与当前窗口不满足同源条件,浏览器为了安全会阻止跨域操作。

CORS是一个W3C标准,全称是“跨域资源共享”(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

了解XSS和CSRF

XSS:跨站脚本(Cross-site scripting,通常简称为XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

CSRF:跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。

用大白话谈谈XSS与CSRF

参考文章:
markyun

@ishowman
Copy link

什么叫野路子前端?上班时经常出去打野那种吗?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants