Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示独一无二的值)和BigInt(es10新增)
;引用数据类型——Object(Object本质上是由一组无序的名值对组成的)。里面包含 Dunction、Array、Date等。
JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。栈中存储了指针,该指针指向堆中该实体的起始地址
。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。只有三种情况,分别是
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
被解释为object类型的还有数组和null
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){
}); // function
console.log(typeof {
}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 object
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型,这时候可以考虑使用 instanceof
instanceof可以正确判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的prototype
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){
} instanceof Function); // true
console.log({
} instanceof Object); // true
// console.log(undefined instanceof Undefined);
// console.log(null instanceof Null);
可以看出instanceof可以精确判断出一个引用数据类型的类型,但是不能精确判断出基本数据类型
因为instanceof是用来测试一个对象在该原型链中是否存在构造函数的prototype属性,也就是判断该对象是否是某一数据类型的实例。
而数字类型,字符串类型,布尔类型不是实例,所以为false。
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {
}).constructor === Function); // true
console.log(({
}).constructor === Object); // true
这也是通过判断构造函数中的prototype来进行的
但是修改prototype过后就会使constructor返回值修改,即使他的原型是本来的,但是原型的prototype改了。
function Fn(){
};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){
}));
console.log(a.call({
}));
console.log(a.call(undefined));
console.log(a.call(null));
用call()对传进去的值进行该值的原型链prototype的构造函数type转换为字符串过后的回调
可以理解成有哪些内核定义的对象
js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
全局的对象( global objects )或称标准内置对象,不要和 “全局对象(global object)” 混淆。这里说的全局的对象是说在
全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。
例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。
例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。
例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。
例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。
例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。
例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
例如 JSON 等
(10)控制抽象对象
例如 Promise、Generator 等
(11)反射
例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。
例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他
例如 arguments
参考链接
作用域链本质是指向变量对象的指针列表。
作用域链的前端始终都是当前执行上下文的变量对象,全局对象始终都是作用域链的最后一个对象
查找变量的时候,如果当前没找到,可以沿着作用域链往后查找
创建过程与执行上下文的建立有关
var Person = new Object();
Person.name = 'Nike';
Person.age = 29;
Person指向用new创建Object引用类型的一个新实例,然后进行值的添加
var Person = {
};//相当于var Person = new Object();
Person = {
name:'Nike';
age:29;
}
用函数封装对象细节,通过调用函数进行复用。创建出来的对象无法和某个类型联系起来,没有建立对象和类型间的关系。
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('Nike',29,'teacher');
var person2 = createPerson('Arvin',20,'student');
只要一个函数通过new调用,就可以称为构造函数。
首先会创建一个对象,然后将对象的原型指向构造函数的prototype,将执行上下文的this值向这个对象,然后用this给对象赋值。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person('Nike',29,'teacher');
var person2 = new Person('Arvin',20,'student');
优点是:
1.创建的对象和构造函数建立了关系,就是prototype,因此可以通过原型来识别对象的类型;也就是说可以通过该对象的原型的prototype判断类型:
2.无显示创建对象
3.直接将属性和方法赋值给了this对象
4.无return
alert(person1 instanceof Object);//ture
alert(person1 instanceof Person);//ture
//instanceof还可以换成
//1.person1.constructor === Object;
//var a = Object.prototype.toString; a.call(person1) === Person
缺点是:每个方法都要在每个实例上重新创建一遍,方法指的就是我们在对象里面定义的函数。如果方法的数量很多,就会占用很多不必要的内存。
function Person(){
}
Person.prototype.name = 'Nike';
Person.prototype.age = 20;
Person.prototype.jbo = 'teacher';
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name ='Greg';
alert(person1.name); //'Greg' --来自实例
alert(person2.name); //'Nike' --来自原型
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 Person('Nike',20,'teacher');
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性
由于JS并不是真正面向对象的语言,是基于对象的,没有类的概念,所以继承就要用JS对象的原型prototype机制或者用apply和call方法实现。
可以把prototype看作一个模板,新创建的对象都是这个模板的拷贝(新实例化的对象内部有个_Proto_指针,指向原型对象)
通过this.某某函数()将parent函数引入,然后对其进行传参操作,此时该子对象this已经有parent的值了,也就是说一个传参后的副本,销毁最开始的parent引入,就只剩this自己的继承属性了
function Parent(username){
this.username = username;
this.hello = function(){
console.log('hello ' + this.username);
}
}
Parent.prototype.sayMorning = function(){
console.log('good morning ' + this.username);
}
function Child(username,password){
//通过以下3行实现将Parent的属性和方法追加到Child中,从而实现继承
//第一步:this.method是作为一个临时的属性,并且指向Parent所指向的对象,
//第二步:执行this.method方法,即执行Parent所指向的对象函数
//第三步:销毁this.method属性,即此时Child就已经拥有了Parent的所有属性和方法
this.method = Parent;
this.method(username);//最关键的一行
delete this.method;
this.password = password;
this.world = function(){
console.log(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
parent.sayMorning();
child.hello();
child.world();
因为call函数能够回调全局对象的函数/全局函数将该函数赋值给传进去的this上,所以call返回的this对象就是进行父函数回调处理过后的对象,就能够实现继承。
但是this的原型还是child的原型!!!因为传进去的this本身就是child
function Parent(username){
this.username = username;
this.hello = function(){
console.log(this.username);
}
}
Parent.prototype.sayMorning = function(){
console.log('good morning ' + this.username);
}
function Child(username,password){
Parent.call(this,username);
this.password = password;
this.world = function(){
console.log(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
parent.sayMorning();
child.hello();
child.world();
// child.sayMorning(); 通过prototype 添加的方法和属性,不能用来继承
原理跟call一样,只是传进去的值第一个是this,第二个必须是有内存分配(不是野指针)的数组;例如new Array()或者 var []
function Parent(username){
this.username = username;
this.hello = function(){
console.log(this.username);
}
}
Parent.prototype.sayMorning = function(){
console.log('good morning ' + this.username);
}
function Child(username,password){
Parent.apply(this,new Array(username));
this.password = password;
this.world = function(){
console.log(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
parent.sayMorning();
child.hello();
child.world();
// child.sayMorning(); 通过prototype 添加的方法和属性,不能用来继承
原型链方式,即子类通过prototype将所有在父类中通过prototype追加的属性和方法都追加到child里面,从而实现继承。
function Person(){
}
Person.prototype.hello = "hello";
Person.prototype.sayHello = function(){
console.log(this.hello);
}
function Child(){
}
Child.prototype = new Person();//这行的作用是:将Parent中将所有通过prototype追加的属性和方法都追加到Child,从而实现了继承
console.log("判断Child的原型链是不是Person原型的一个实例化对象 " + (Child.prototype.__proto__ === Person.prototype))
//判断Child的原型链是不是Person原型的一个实例化对象 true
Child.prototype.world = "world";
Child.prototype.sayWorld = function(){
console.log(this.world);
}
var c = new Child();
c.sayHello();
c.sayWorld();//undefined
缺点1:通过prototype 添加的方法和属性,不能用来继承!!!!为什么呢??下面有解释
Child.prototype = new Person();
进行解释:因为子对象的原型是父对象的原型的实例化对象,所以不能继承父对象的方法和属性。
function Parent(hello){
this.hello = hello;
}
Parent.prototype.sayHello = function(){
console.log(this.hello);
}
function Child(hello,world){
Parent.call(this,hello);//将父类的属性继承过来
this.world = world;//新增一些属性
}
Child.prototype = new Parent();//将父类的方法继承过来
Child.prototype.sayWorld = function(){
//新增一些方法
console.log(this.world);
}
var c = new Child("zhangsan","lisi");
c.sayHello();
c.sayWorld();
可以继承通过prototype 添加的方法和属性
为什么呢
因为在子对象的初始化时,加了一个父类型的原型属性回调
Parent.call(this,hello);//将父类的属性继承过来
这样就强行把父类型的原型属性拉了过来
js中call和apply都可以实现继承,唯一的一点参数不同,
func.call(func1,var1,var2,var3)
对应的apply写法为:func.apply(func1,[var1,var2,var3])
。
调用一个对象的一个方法,以另一个对象替换当前对象。
call([thisObj[,arg1[, arg2[, [,.argN]]]]])
参数:thisObj 可选项 默认就是this 被称作当前对象的对象
args 可选项 将被传递方法参数序列
call方法可以用来代替另一个对象调用一个方法,将一个函数的对象上下文,从初始的上下文改变为thisObj指定的新对象。也就是说这个第三方函数对象的上下文this指向的是第一方函数作用链外的那个对象的this。
说简单点,这两个函数的作用其实就是更改调用的函数对象的内部指针,即改变对象的this指向的内容,使调用的第三方函数对象的this指向作用域链外的那个对象的this。
function Person(name,age){
//定义一个类
this.name=name; //名字
this.age=age; //年龄
this.sayhello=function(){
alert(this.name)};
}
function Print(){
//显示类的属性
this.funcName="Print";
this.show=function(){
var msg=[];
for(var key in this){
if(typeof(this[key])!="function"){
msg.push([key,":",this[key]].join(""));
}
}
alert(msg.join(" "));
};
}
function Student(name,age,grade,school){
//学生类
Person.apply(this,arguments);//比call优越的地方
Print.apply(this,arguments);
this.grade=grade; //年级
this.school=school; //学校
}
var p1=new Person("卜开化",80);
p1.sayhello();
var s1=new Student("白云飞",40,9,"岳麓书院");
s1.show();
s1.sayhello();
alert(s1.funcName);
//alert
卜开化
name:白云飞 age:40 funcName:Print grade:9 school:岳麓书院
白云飞
Print
另外,Function.apply()在提升程序性能方面有突出作用:
我们先从Math.max()函数说起,Math.max后面可以接任意个参数,最后返回所有参数中的最大值。
alert(Math.max(5,8)); //8
alert(Math.max(5,7,9,3,1,6)); //9
//但是在很多情况下,我们需要找出数组中最大的元素。
var arr=[5,7,9,1];
//alert(Math.max(arr)); // 这样却是不行的。NaN
//要这样写
function getMax(arr){
var arrLen=arr.length;
for(var i=0,ret=arr[0];i<arrLen;i++){
ret=Math.max(ret,arr[i]);
}
return ret;
}
alert(getMax(arr)); //9
//换用apply,可以这样写
function getMax2(arr){
return Math.max.apply(null,arr);
}
alert(getMax2(arr)); //9
//两段代码达到了同样的目的,但是getMax2却优雅,高效,简洁得多。
//再比如数组的push方法。
var arr1=[1,3,4];
var arr2=[3,4,5];
//如果我们要把 arr2展开,然后一个一个追加到arr1中去,最后让arr1=[1,3,4,3,4,5]
//arr1.push(arr2)显然是不行的。 因为这样做会得到[1,3,4,[3,4,5]]
//我们只能用一个循环去一个一个的push(当然也可以用arr1.concat(arr2),但是concat方法并不改变arr1本身)
var arrLen=arr2.length;
for(var i=0;i<arrLen;i++){
arr1.push(arr2[i]);
}
//自从有了Apply,事情就变得如此简单
Array.prototype.push.apply(arr1,arr2); //现在arr1就是想要的结果
//alert 8 9 9 9
p.__proto__
p.constructor.prototype
也就是Person.prototypeObject.getPrototypeOf(p)
闭包就是有权访问另一个函数作用域内变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量
闭包的两个常见用途
function a(){
var n = 0;
function add(){
n++;
console.log(n);
}
return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1(); //1
a1(); //2 第二次调用n变量还在内存中
事件是用户和网页的交互
本质上是利用了浏览器冒泡的机制,因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因为可以把子节点的监听函数定义在父节点上,有父节点的监听函数统一处理各个子元素的事件
也就是父元素对于子元素的事件代理
不必每个元素都绑定监听事件,这样减少了内存的消耗和内存泄漏。
使用事件代理可以实现事件的动态绑定,比如新增节点就不需要为他再添加一个监听事件,发生的事件会交给父元素的监听函数来处理
当事件发生再DOM元素上时,该事件并不完全发生再那个元素上
事件传播有三个阶段,
当事件发生再DOM元素上时,该事件并不完全发生再那个与那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素,window–document–html–body–目标元素
例如有下面的HTML结构
有三层继承关系
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
我们来写一个模拟事件捕获的js代码
function addEvent(element,event,callback,isCapture = false){
//判断输入是否错误
if(!element || !event ||!callback || typeof callback !== 'function') return;
if(typeof element === 'string'){
element = document.querySelector(element);
};
element.addEventListener(event ,callback ,isCapture);
}
addEvent(document,'DOMContentLoaded',()=>{
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');
addEvent(child,'click',function(e){
console.log('child');
}
);
addEvent(parent,'click',function(e){
console.log('parent');
}
);
addEvent(grandparent,'click',function(e){
console.log('grandparent');
}
);
addEvent(document,'click',function(e){
console.log('document');
}
);
addEvent('html', 'click', function (e) {
console.log('html');
})
addEvent(window, 'click', function (e) {
console.log('window');
})
});
addEventlistener方法具有第三个可选参数useCapture,默认值为false,事件将在冒泡阶段中发生,如果为true 则事件将在捕获阶段中发生,如果单击child元素,它将分别在控制台上打印window document html grandparent parent
也就是模拟出了一个事件捕获
事件冒泡刚好与事件捕获相反,当前元素–body–html–document–window
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)
getElementById();
getElementByName();
getElementByTagName();
getElementByClassName();
querySelector();
querySelectorAll();
4.属性操作
getAttribute(key);
getAttribute(key,value);
hasAttribute(key);
document.getElementsByTagName("BUTTON")[0].hasAttribute("onclick");
removeAttribute(key)
concat()返回一个由两个数组合并组成的新数组
join()返回一个由数组中的所有元素连接在一起的String对象
pop()删除数组中的最后一个元素并返回该值
push()向数组中添加新元素 返回新长度
shift()删除数组中的第一个元素并返回该值
unshift返回一个数组,在该数组头部插入指定的元素
sort()返回一个元素被排序了的Array对象
reverse()返回一个元素反序的Array对象
splice(index.num,foo...)返回数组的一个片段 --剪切
slice(start,end)复制 [start,end) 负数的时候 lenght+start/end
length 属性 长度
concat(String) 连接两个或更多个字符
indexOf(string) 返回出现字符的位置
substr(num1,[num2])截取字符串
toLowerCase()转成小写
toUpperCase()转成大写
replace(str1,str2) 字符串替换
cell(数值)大于或等于该数的最小整数
floor(数值)小于或等于该数的最大整数
min(数值1,数值2)返回最小值
max(数值1,数值2)返回最大值
pow(数值1,数值2)返回数值1的数值2的次方
random()返回随机数0--1
round(数值)四舍五入
sqrt(数值)开平方根
//(1)匹配 16 进制颜色值
var color = /#([0-9a-fA-F]{
6}|[0-9a-fA-F]{
3})/g;
//(2)匹配日期,如 yyyy-mm-dd 格式
var date = /^[0-9]{
4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
//(3)匹配 qq 号
var qq = /^[1-9][0-9]{
4,10}$/g;
//(4)手机号码正则
var phone = /^1[34578]\d{
9}$/g;
//(5)用户名正则
var username = /^[a-zA-Z\$][a-zA-Z0-9_\$]{
4,16}$/;
//(6)Email正则
var email = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{
2,4})$/;
//(7)身份证号(18位)正则
var cP = /^[1-9]\d{
5}(18|19|([23]\d))\d{
2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{
3}[0-9Xx]$/;
//(8)URL正则
var urlP= /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{
2,6})([\/\w \.-]*)*\/?$/;
// (9)ipv4地址正则
var ipP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){
3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// (10)//车牌号正则
var cPattern = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{
1}[A-Z]{
1}[A-Z0-9]{
4}[A-Z0-9挂学警港澳]{
1}$/;
// (11)强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):var pwd = /^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$/
一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
创建XMLHttpRequest异步对象
var xhr = new XMLHttpRequest()
设置回调函数
xhr.onreadystatechange = callback
使用open方法与服务器建立连接
// get 方式
xhr.open("get", "test.php", true)
// post 方式发送数据 需要设置请求头
xhr.open("post", "test.php", true)
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
向服务器发送数据
// get 不需要传递参数
xhr.send(null)
// post 需要传递参数
xhr.send("name=jay&age=18")
在回调函数中针对不同的响应状态进行处理
function callback() {
// 判断异步对象的状态
if(xhr.readyState == 4) {
// 判断交互是否成功
if(xhr.status == 200) {
// 获取服务器响应的数据
var res = xhr.responseText
// 解析数据
res = JSON.parse(res)
}
}
}
$.ajax({
type:'post',
url:'',
async:ture,//async 异步 sync 同步
data:data,//针对post请求
dataType:'jsonp',
success:function (msg) {
},
error:function (error) {
}
})
// 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;
}
js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。
// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此处略去 100 行
var b = require("./b"); // 依赖可以就近书写
b.doSomething();
// ...
});
// AMD 默认推荐
define(["./a", "./b"], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
// ...
})
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。
1.单线程
因为是 脚本语言 和 要操作DOM 就决定了他是单线程
因为浏览器为了避免JS同时两个或多个线程,例如:一个线程在DOM节点上增加内容,另一个线程删除了某节点;
2.事件循环
同步任务:例如页面构建DOM树和元素渲染
异步任务:像加载图片音乐之类占用资源大耗时久的任务
那么主线程什么时候为空呢?
js引擎存在monitoring process进程, 会不断检查主线程执行栈是否为空,空了就会去阻塞函数进行回调
需要注意的是,除了同步和异步,还可以分为宏任务、微任务,JS引擎优先执行微任务
面试的推荐回答
例如以下代码:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
第一轮
第二轮
最终结果是:25431
是函数中传递的参数集合。是类似数组的对象,因为他有length属性,可以用数组索引法arguments[0]来访问某个值
但他没有数组的内置方法,如:forEach、reduce、filter、map
可以使用Array.prototype.slice.call(arguments)
,转换成一个数组
注意 剪头函数 没有 arguments 对象
function one() {
return arguments;
}
const two = function () {
return arguments;
}
const three = function three() {
return arguments;
}
const four = () => arguments;
four(); // Throws an error - arguments is not defined
使用rest解构语法,可以解决这个问题。
const four = (...args) => args;
自动将参数值放入数组中
function myFunc(){
let a = b = 0;
}
myFunc();
实际上是
function myFunc() {
let a = (b = 0);
}
myFunc();
原因是赋值运算符是从右到左求值的,当多个赋值运算符出现在一个表达式中时,它们从右向左求值。
我们可以通过在赋值之前声明变量来解决这个全局污染的问题
function myFunc() {
let a,b;
a = b = 0;
}
myFunc();
基于分代回收机制,基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久
新生代
新创建的对象或者之经历过一次的垃圾回收对象被称为新生代,经历过多次垃圾回收的对象被称为老生代
新生代被分为From和To两个空间,To一般是闲置的。From空间满了的会执行Scavenge算法进行垃圾回收。当我们执行垃圾回收算法的时候,应用逻辑就会停止,等垃圾回收结束后再继续执行
Scavenge算法
首先检查From空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间
如果对象不存活,则释放对象空间
最后将From空间和To空间角色进行交换
晋升有两个条件
1.判断对象是否经历过一次回收,如果经历过就从From空间复制到老生代中,没有经历就复制到To空间
2.To空间的内存使用占比是否超过限制,当对象从From空间复制到To空间,如果To空间超过25%就是说要满了,那么直接晋升到老生代中(草草草真就被卷走了呗),因为算法结束过后两个空间会交换位置,如果内存太小会影响后续的内存分配。
老生代
采用标记清楚法和标记压缩法。首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清楚后会造成很多的内存碎片。不便于后面的内存分配,所以为了解决内存碎片引入了标记压缩法
由于再进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响,为了解决这个问题V8引入了增量标记的方法,讲一次停顿进行的过程分成了多步,完成了一小步就让运行逻辑执行一会儿,这样就能交替运行
ECMAScript 是编写脚本语言的标准,这意味着JavaScript遵循ECMAScript标准中的规范变化,因为它是JavaScript的蓝图。
ECMAScript 和 Javascript,本质上都跟一门语言有关,一个是语言本身的名字,一个是语言的约束条件 只不过发明JavaScript的那个人(Netscape公司),把东西交给了ECMA(European Computer Manufacturers Association),这个人规定一下他的标准,因为当时有java语言了,又想强调这个东西是让ECMA这个人定的规则,所以就这样一个神奇的东西诞生了,这个东西的名称就叫做ECMAScript。
javaScript = ECMAScript + DOM + BOM(自认为是一种广义的JavaScript)
ECMAScript说什么JavaScript就得做什么!
JavaScript(狭义的JavaScript)做什么都要问问ECMAScript我能不能这样干!如果不能我就错了!能我就是对的!——突然感觉JavaScript好没有尊严,为啥要搞个人出来约束自己,
var 声明的变量会挂载在window上,而let和const声明的变量不会
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
var声明变量存在变量提升(变量可以先使用再声明),let和const不存在变量提升(
use strict;
严格模式下就不行哦 )
console.log(a); // undefined ===> a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined ===> 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined ===> 找不到c这个变量
const c = 10;
let和const声明形成块作用域 var挂载在window
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
-------------------------------------------------------------
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100
console.log(c) // 报错:c is not defined ===> 找不到c这个变量
同一作用域下let和const不能声明同名变量,而var可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;
// 控制台报错:Identifier 'a' has already been declared ===> 标识符a已经被声明了。
暂存死区
var a = 100;
if(1){
a = 10;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
const
/*
* 1、一旦声明必须赋值,不能使用null占位。
*
* 2、声明后不能再修改
*
* 3、如果声明的是复合类型数据(引用数据类型),可以修改其属性
*
* */
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {
a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
箭头函数没有自己的this,arguments,super或new.target
,更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数
//ES5 Version
var getCurrentDate = function (){
return new Date();
}
//ES6 Version
const getCurrentDate = () => new Date();
只需要()括号,不需要 return 语句
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
const greet = (name) => `Hello ${
name}`;
const greet2 = name => `Hello ${
name}`;
使用与函数表达式和函数声明相同的参数
箭头函数不能访问arguments对象。所以调用第一个getArgs函数会抛出一个错误。相反,我们可以使用rest参数来获得在箭头函数中传递的所有参数。
const getArgs = () => arguments
const getArgs2 = (...rest) => rest
const data = {
result: 0,
nums: [1, 2, 3, 4, 5],
computeResult() {
// 这里的“this”指的是“data”对象
const addAll = () => {
return this.nums.reduce((total, cur) => total + cur, 0)
};
this.result = addAll();
}
};
箭头函数没有自己的this值。它捕获词法作用域函数的this值,在此示例中,addAll函数将复制computeResult 方法中的this值,如果我们在全局作用域声明箭头函数,则this值为 window 对象。
类(class)是在 JS 中编写构造函数的新方法。它是使用构造函数的语法糖,在底层中使用仍然是原型和基于原型的继承。
//ES5 Version
function Person(firstName, lastName, age, address){
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
}
Person.self = function(){
return this;
}
Person.prototype.toString = function(){
return "[object Person]";
}
Person.prototype.getFullName = function (){
return this.firstName + " " + this.lastName;
}
//ES6 Version
class Person {
constructor(firstName, lastName, age, address){
this.lastName = lastName;
this.firstName = firstName;
this.age = age;
this.address = address;
}
static self() {
return this;
}
toString(){
return "[object Person]";
}
getFullName(){
return `${
this.firstName} ${
this.lastName}`;
}
}
重写方法并从另一个类继承。
//ES5 Version
Employee.prototype = Object.create(Person.prototype);
function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
Person.call(this, firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
Employee.prototype.describe = function () {
return `I am ${
this.getFullName()} and I have a position of ${
this.jobTitle} and I started at ${
this.yearStarted}`;
}
Employee.prototype.toString = function () {
return "[object Employee]";
}
//ES6 Version
class Employee extends Person {
//Inherits from "Person" class
constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
super(firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
describe() {
return `I am ${
this.getFullName()} and I have a position of ${
this.jobTitle} and I started at ${
this.yearStarted}`;
}
toString() {
// Overriding the "toString" method of "Person"
return "[object Employee]";
}
}
所以我们要怎么知道它在内部使用原型?
class Something {
}
function AnotherSomething(){
}
const as = new AnotherSomething();
const s = new Something();
console.log(typeof Something); // "function"
console.log(typeof AnotherSomething); // "function"
console.log(as.toString()); // "[object Object]"
console.log(as.toString()); // "[object Object]"
console.log(as.toString === Object.prototype.toString); // true
console.log(s.toString === Object.prototype.toString); // true
模板字符串是在 JS 中创建字符串的一种新方法。
我们可以通过使用反引号使模板字符串化。
//ES5 Version
var greet = 'Hi I\'m Mark';
//ES6 Version
let greet = `Hi I'm Mark`;
在 ES5 中我们需要使用一些转义字符来达到多行的效果,在模板字符串不需要这么麻烦:
//ES5 Version
var lastWords = '\n'
+ ' I \n'
+ ' Am \n'
+ 'Iron Man \n';
//ES6 Version
let lastWords = `
I
Am
Iron Man
`;
在 ES5 版本中,如果需要在字符串中添加表达式或值,则需要使用+运算符。在模板字符串s中,我们可以使用${expr}嵌入一个表达式,这使其比 ES5 版本更整洁
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
function greet(name) {
return `Hello ${
name} !`;
}
假设有如下的对象
const employee = {
firstName: "Marko",
lastName: "Polo",
position: "Software Developer",
yearHired: 2017
};
es5之前获取对象属性时
var firstName = employee.firstName;
var lastName = employee.lastName;
var position = employee.position;
var yearHired = employee.yearHired;
使用解构就方便多了
{
firstName, lastName, position, yearHired } = employee;
我们还可以为属性取别名:
let {
firstName: fName, lastName: lName, position, yearHired } = employee;
当然如果属性值为 undefined 时,我们还可以指定默认值:
let {
firstName = "Mark", lastName: lName, position, yearHired } = employee;
Set 对象允许你存储任何类型的唯一值
,无论是原始值或者是对象引用。
我们可以使用Set构造函数创建Set实例。
const set1 = new Set();
const set2 = new Set(["a","b","c","d","d","e"]);
使用add方法向Set实例中添加一个新值,因为add方法返回Set对象,所以我们可以以链式的方式再次使用add,如果一个值已经存在于Set对象中,那么它将不再被添加。
set2.add("f");
set2.add("g").add("h").add("i").add("j").add("k").add("k");
// 后一个“k”不会被添加到set对象中,因为它已经存在了
我们可以使用has方法检查Set实例中是否存在特定的值。
set2.has("a") // true
set2.has("z") // true
我们可以使用size属性获得Set实例的长度。
set2.size // returns 10
可以使用clear方法删除 Set 中的数据。
set2.clear();
我们可以使用Set对象来删除数组中重复的元素。
const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
const uniqueNums = [...new Set(numbers)]; // [1,2,3,4,5,6,7,8]
另外还有WeakSet, 与 Set 类似,也是不重复的值的集合。但是 WeakSet 的成员只能是
对象
,而不能是其他类型的值。WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet对该对象的引用
。
对要保护的对线套一层,不被外界所访问。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截
,因此提供了一种机制,可以对外界的访问进行过滤和改写
let obj={
time:'2017-03-11',
name:'net',
_r:123
};
let monitor=new Proxy(obj,{
// 拦截对象属性的读取
get(target,key){
return target[key].replace('2017','2018')
},
// 拦截对象设置属性
set(target,key,value){
if(key==='name'){
return target[key]=value;
}else{
return target[key];
}
},
// 拦截key in object操作
has(target,key){
if(key==='name'){
return target[key]
}else{
return false;
}
},
// 拦截delete
deleteProperty(target,key){
if(key.indexOf('_')>-1){
delete target[key];
return true;
}else{
return target[key]
}
},
// 拦截Object.keys,Object.getOwnPropertySymbols,Object.getOwnPropertyNames
ownKeys(target){
return Object.keys(target).filter(item=>item!='time')
}
});
console.log('get',monitor.time);
monitor.time='2018';
monitor.name='poetries';
console.log('set',monitor.time,monitor);
console.log('has','name' in monitor,'time' in monitor);
// delete monitor.time;
// console.log('delete',monitor);
//
// delete monitor._r;
// console.log('delete',monitor);
console.log('ownKeys',Object.keys(monitor));
const EventUtils = {
// 视能力分别使用dom0||dom2||IE方式 来绑定事件
// 添加事件
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} 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;
}
},
// 获取事件目标
getTarget: function(event) {
return event.target || event.srcElement;
},
// 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
getEvent: function(event) {
return event || window.event;
},
// 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
// 取消事件的默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
函数式编程(通常缩写为FP)是通过编写纯函数,避免共享状态、可变数据、副作用 来构建软件的过程。数式编程是声明式 的而不是命令式 的,应用程序的状态是通过纯函数流动的。与面向对象编程形成对比,面向对象中应用程序的状态通常与对象中的方法共享和共处。
函数式编程是一种编程范式 ,这意味着它是一种基于一些基本的定义原则(如上所列)思考软件构建的方式。当然,编程范式的其他示例也包括面向对象编程和过程编程。
函数式的代码往往比命令式或面向对象的代码更简洁,更可预测,更容易测试 - 但如果不熟悉它以及与之相关的常见模式,函数式的代码也可能看起来更密集杂乱,并且 相关文献对新人来说是不好理解的。
function higherOrderFunction(param,callback){
return callback(param);
}
函数不仅拥有一切传统函数的使用方式(声明和调用),而且可以做到像简单值一样
var func = function(){}
function func(x,callback){callback(x);}
function(){return function(){}}
这样的函数被称为第一级函数。
很多函数都充当了类的构造函数的作用,同时又是一个Function类的实例。
map()方法创建一个新数组,其结果是该数组的每个元素都调用一个提供的函数后返回的结果
function map(arr,mapCallback){
//首先检查传递的是不是array和callback函数
if(!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function'){
return [];
}else{
//每次调用此函数时,都会创建一个result数组,并返回;因为map就是不会改变原数组,就像bind一样
let result = [];
for(let i = 0, len = arr.length; i < len; i++){
//map方法作用还有将会返回数组每个被mapCallback处理过后的值,所以在新建的result数组中挨个push
result.push(mapCallback(arr[i],u,arr));//数组元素 元素索引 原数组
}
return result;
}
}
filter()方法创建一个新的过滤后的数组,包含通过所提供的函数实现的测试的所有元素
过滤的意思是:满足某个条件就返回 没满足就去除
function filter(arr,filterCallback){
//首先检查传递的参数是否是有长度的数组和过滤函数
if(!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function'){
return [];}
else{
//因为创建新数组所以块级作用域新建
let result = [];
for(let i = 0, len = arr.length, i++){
//检查filterCallback 返回值是否为真值 也就是传入的参数是否满足过滤条件
if(filterCallback(arr[i], i, arr)){
result.push(arr[i]);
}
}
return result;
}
}
reduce()方法对数组的每个元素执行一个提供的reducer函数(升序执行),将结果汇总为单个返回值
function reduce(arr, reduceCallback, initialValue){
//首先,检查传递的参数是否正确
if(!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function'){
return []; }
else{
//如果没有将initialValue传递给该函数,我们将使用第一个数组项作为initial Value
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : arr[0];
}
//如果有传递inisialValue,则索引从1开始,否则从0开始
for(let i = hasInitialValue ? 1 : 0,len = arr.length; i < len; i
++){
value = reduceCallback(value, arr[i], i, arr);
}
return value;
}
浅拷贝:
创建一个新对象,这个对象有原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值;
如果是引用类型,拷贝的就是内存地址;
所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存开辟新的区域存放新对象,且修改新对象不会影响原对象,
浅拷贝实现方式
let a = {
name:'Sirius',
flag:{
title:"better day by day",
time:"2020-12-09"
}
}
let b = {
...a};
var a = {
key1:"111"
}
function copy(p){
// console.log(p)
var c = {
};
for(var i in p){
c[i] = p[i];
// console.log(i) // key1
}
return c;
}
// copy(a);
a.key2 = ["you","me"];
var b = copy(a);
b.key3 = "ours";
console.log(a.key3); // undefined
console.log(b.key3) // ours
// console.log(b) // {key1:"111",key2:["you","me"]}
b.key2.push("us")
console.log(b.key2); // ["you", "me", "us"]
console.log(a.key2); // ["you", "me", "us"]
深拷贝的实现方式
function cloneDeep(target, map = new WeakMap()){
//判断是不是object
if(typeof target === 'object'){
//判断是不是数组或object
let cloneTarget = Array.isArray(target) ? [] : {
};
//如果取得到值
if(map.get(target)){
return target;
}
//取不到就说明这个对象里面的target还是对象,就是这个对象不是一维的要进行维度展开
map.set(target,cloneTarget);
for(const key in target){
cloneTarget[key] = cloneDeep(target[key],map);
}
return cloneTarget;
}
}
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
function getType(target) {
return Object.prototype.toString.call(target);
}
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
function cloneFunction(func) {
const bodyReg = /(?<={
)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{
)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}
// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value, map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value, map));
});
return cloneTarget;
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
module.exports = {
clone
};
call函数实现步骤:
//call函数实现
Function.prototype.myCall = function(context){
//判断调用对象
if(typeof this !== 'function'){
console.error("type error");
return null;
}
//获取参数
let args = [...arguments].slice(1),
result = null;
//判断context是否传入 如果未传入 则设置为window
context = context || window;
//将调用函数设为对象的方法
context.fn = this;
//调用函数
result = context.fn(...args);
//将属性删除,因为只需要结果
delete context.fn;
return result;
}
apply函数实现步骤
判断调用对象是否是函数对象,即使我们是定义在函数的原型上的,到那时可能出现使用call等方式调用的情况
判断传入上下文对象是否存在,如果不存在,则设置为window
将函数作为上下文对象的一个属性
判断参数值是否传入
使用上下文对象来调用这个方法,并保存返回结果
删除刚才在上下文对象中新建的这个属性,因为只要结果并不想在上下文上增加
返回结果
//apply函数实现
Function.prototype.myApply = function(context){
//判断调用对象是否是函数
if(typeof this !== 'function'){
throw new TypeError("Error");
}
let result = null;
//判断context是否存在,如果没有传入则挂载到window
context = context || window ;
//给上下文对象建立一个指向此方法的指针
context.fn = this;
//调用方法
if(arguments[1]){
result = context.fn(...arguments[1]);
}else{
result = context.fn();
}
//删除这个指针
delete context.fn();
return result;
}
//bind函数实现
Function.prototype.myBind = function(context){
//判断调用对象是否是函数
if(typeof this !== 'function'){
throw new TypeError("Error");
}
//获取参数
let args = [...arguments].slice(1),
fn = this;
return function Fn(){
//根据调用方式 传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
)
}
}
bind的特点:
- 接受调用传参和新建对象构造函数传参
- 如果是外部没有传入this就要新建一个this
- 和call接受的参数和实现的回调功能一样
- 返回的是一个新创建的原来传入的this克隆的对象
//函数的构造函数 : Function
//用es6解构rest的方式传参
Function.prototype.myBind = function(objThis,...params){
//定义指向当前函数this的指针
const thisTn = this;
//因为最终返回的是一个函数,所以声明一个函数返回
//用let表示块作用域
let funcForBind = function(...secondParams){
/*因为例如
let sayClone = obj.say.bind(obj2,1,2,3);
sayClone(4);
这里还是可以进行传参,最终传的就是1,2,3,4,所以可以用解构 ...secondParams
*/
//判断是不是新创建的对象 如果新创建的对象的话 this就是当前函数的this 不是新创建的话就是传进来的那个对象的上下文
const isNew = this instanseof funcForBind;
const thisArg = isNew ? this : objThis;
//返回调用 并分别解构传入的参数params和创建对象传入的参数secondParams
return thisFn.call(thisArg,...params,...secondParams);
}
//因为bind返回的是克隆的对象,所以还要把原型链进行克隆
funForBind.prototype = Object.create(thisFn.prototype);
return funcForBind;
}
//给obj2增加一个obj的call的函数,然后用传入的参数进行调用返回最终值
obj.say.call(obj2,1,2,3);
Function.prototype.myCall = function(thisArg,...arr){
if(thisArg == null || thisArg == undefined){
thisArg = window;
}
//定义一个不重复的方法名称
const specialMethod = Symbol('anything');
//将这个不重复的方法Function的指针给thisArg的specialMethod方法
thisArg[specialMethod] = this;
//调用函数并结果返回
let result = thisArg[specialMethod](...arr);
//delete 新增的属性
delete thisArg[specialMethod];
return result;
}
obj.say.myCall(obj2,1,2,3);
Function.prototype.myApply = function(thisArg,arr){
if(thisArg == null || thisArg == undefined){
thisArg = window;
}
//定义一个不重复的方法
const specialMethod = Symbol('anything');
//将这个不重复的方法的指针给thisArg的specialMethod方法
thisArg[specialMethod] = this;
//调用函数并结果返回
let result = thisArg[specialMethod](...arr);
//delete 新增的属性
delete thisArg[specialMethod];
return result;
}
obj.say.myApply(obj2,[1,2,3]);
function curry(fn, args){
//获取函数需要的参数长度
let length = fn.length;
args = args || [];
return function(){
let subArgs = args.slice(0);
//拼接所有参数,不仅是curry的,还有fn的
for(let i = 0; i < arguments.length; i++){
subArgs.push(arguments[i]);
}
//判断参数的长度是否已经满足函数所需参数的长度
if(subArgs.length >= length){
//如果满足,执行函数
return fn.apply(this, subArgs);
}else{
//如果不满足,递归返回柯里化的函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
//es6实现
function curry(fn, ...args){
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
创建一个用户定义的对象类型的实例 或 具有构造函数的内置对象的实例
new关键字会进行如下的操作:
{}
第一步:创建一个简单空对象
var obj = {
}
第二步:链接该对象到另一个对象(原型链)
// 设置原型链
obj.__proto__ = Dog.prototype
第三步:将步骤1新创建的对象作为 this 的上下文
// this指向obj对象
Dog.apply(obj, ['大黄', 'yellow', 3])
第四步:如果该函数没有返回对象,则返回this
// 因为 Dog() 没有返回值,所以返回obj
var dog = obj
dog.getName() // '大黄'
需要注意的是如果 Dog() 有 return 则返回 return的值
var rtnObj = {
}
function Dog(name, color, age) {
// ...
//返回一个对象
return rtnObj
}
var dog = new Dog('大黄', 'yellow', 3)
console.log(dog === rtnObj) // true
接下来我们将以上步骤封装成一个对象实例化方法,即模拟new的操作:
function objectFactory(){
var obj = {
};
//取得该方法的第一个参数(并删除第一个参数),该参数是构造函数
var Constructor = [].shift.apply(arguments);
//将新对象的内部属性__proto__指向构造函数的原型,这样新对象就可以访问原型中的属性和方法
obj.__proto__ = Constructor.prototype;
//取得构造函数的返回值
var ret = Constructor.apply(obj, arguments);
//如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
return typeof ret === "object" ? ret : obj;
}
const btnAdd = document.getElementById('btnAdd');
btnAdd.addEventListener('click', function clickCallback(e) {
// do something useless
});
在本例中,我们等待id为btnAdd的元素中的click事件,如果它被单击,则执行clickCallback函数。回调函数向某些数据或事件添加一些功能。
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个事件存在依赖性:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
这就是典型的回调地狱,以上代码看起来不利于阅读和维护,事件一旦多起来就更是乱糟糟,所以在es6中提出了Promise和async/await来解决回调地狱的问题。当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。接下来的两条就是来解决这些问题的,咱们往下看。
Promise,翻译过来是承诺,承诺它过一段时间会给你一个结果。从编程讲Promise 是异步编程的一种解决方案。下面是Promise在MDN的相关说明:
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
一个 Promise有以下几种状态:
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 fulfilled/rejected 后,就不能再次改变。可能光看概念大家不理解Promise,我们举个简单的栗子;
我们将上一条回调地狱的代码改写一下:
new Promise((resolve,reject) => {
setTimeout(() => {
console.log(1)
resolve()
},1000)
}).then((res) => {
setTimeout(() => {
console.log(2)
},2000)
}).then((res) => {
setTimeout(() => {
console.log(3)
},3000)
}).catch((err) => {
console.log(err)
})
Promise也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
promise手写实现,面试够用版:
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定义链式调用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
Iterator是理解第61条的先决知识,也许是我IQ不够,Iterator和Generator看了很多遍还是一知半解,即使当时理解了,过一阵又忘得一干二净。。。
Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator语法:
const obj = {
[Symbol.iterator]:function(){
}
}
[Symbol.iterator] 属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。
迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 方法,改变指针的指向,让其指向下一条数据
每一次的 next 都会返回一个对象,该对象有两个属性
value 代表想要获取的数据
done 布尔值,false表示当前指针指向的数据有值,true表示遍历已经结束
Iterator 的作用有三个:
let arr = [{
num:1},2,3]
let it = arr[Symbol.iterator]() // 获取数组中的迭代器
console.log(it.next()) // { value: Object { num: 1 }, done: false }
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }
Generator函数可以说是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
上面这个示例就是一个Generator函数,我们来分析其执行过程:
首先 Generator 函数调用时它会返回一个迭代器
当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
Generator 函数一般见到的不多,其实也于他有点绕有关系,并且一般会配合 co 库去使用。当然,我们可以通过 Generator 函数解决回调地狱的问题。
async/await是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。毕竟一直then()也很烦。
async 是异步的意思,而 await 是 async wait的简写,即异步等待。
所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function test(){
return "1"
}
console.log(test())// -> Promise {
<resolved>: "1"}
可以看到输出的是一个Promise对象。所以,async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve() 封装成Promise对象返回。
相比于 Promise,async/await能更好地处理 then 链
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${
n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${
n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${
n}`);
return takeLongTime(n);
}
现在分别用 Promise 和async/await来实现这三个步骤的处理。
使用Promise
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${
result}`);
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
使用async/await
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${
result}`);
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,优雅整洁,几乎跟同步代码一样。
await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。
优缺点:
async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。
当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
实现 instanceof:
通过比较实例化对象a的__proto__
属性也就是a的属性是不是B的构造函数prototype
属性。
function myInstanceof(left, right) {
let prototype = right.prototype
left = left.__proto__
while (true) {
if (left === null || left === undefined)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
避免因为用户的多次点击向后端发送多次请求
。只能有一次触发事件的回调函数执行
,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在scroll函数的事件监听上,通过事件节流来降低事件调用的频率;呃呃或者是缓存的expire上// 函数防抖的实现
function debounce(fn, wait) {
var timer = null;
return function() {
var context = this,
args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定时间后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
// 函数节流的实现;
function throttle(fn, delay) {
var preTime = Date.now();
return function() {
var context = this,
args = arguments,
nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}
文章来源:FE- interview