js知识总结—常用功能代码实现

2019
05/03

这里是一些常用的功能、工具类代码写法,长期完善。

1.防抖

触发高频事件后n秒内函数只会执行一次(执行最后一次),如果n秒内高频事件再次被触发,则重新计算时间。

将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

实现方式:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法

缺点:如果事件在规定的时间间隔内被不断的触发,则调用方法会被不断的延迟

//防抖debounce代码:
function debounce(fn, delay = 500) {
    let timeout = null; // 创建一个标记用来存放定时器的返回值
    // 闭包返回的函数使用普通函数而非箭头函数,是为了其中this指向赋给传入的fn,否则this将指向外层
    return function () {
        // 每当用户输入的时候把前一个 setTimeout clear 掉
        clearTimeout(timeout); 
        // 然后又创建一个新的 setTimeout, 这样就能保证interval 间隔内如果时间持续触发,就不会执行 fn 函数
        // 定时器中钩子使用箭头函数便于里面this指向外层function
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, delay);
    };
}
// 处理函数
function handle() {
    console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle));

2.节流

高频事件触发,但在n秒内只会执行一次(执行第一次),所以节流会稀释函数的执行频率。

使得一定时间内只触发一次函数。原理是通过判断是否有延迟调用函数未执行。

实现方式:每次触发事件时,如果当前有等待执行的延时函数,则直接return。

特点:在一定时间内只存在一个定时器,执行完毕才能开启下次计时。

这里的this和arguments可以用es6箭头函数代替。

//节流throttle代码:
function throttle(fn,delay) {
    let timer = null; // 通过闭包保存一个标记
    return function () {
         // 在函数开头判断是否已有定时,若已经开启则直接返回
        if (timer) return;
        // 将外部传入的函数的执行放在setTimeout中
        timer = setTimeout(() => { 
        // 最后在setTimeout执行完毕后再把标记设置null,表示可以执行下一次循环了。
        // 在一定时间内只存在一个定时器,执行完毕才能开启下次计时
            fn.apply(this, arguments);
            timer = null;
        }, delay);
    };
}

//用时间戳实现节流
function throttle(handler, delay) {
    let lastTime = Date.now();
    return function () {
        let nowTime = Date.now();
        if (nowTime - lastTime > delay) {
            handler.apply(this, arguments);
            lastTime = nowTime;
        }
    }
}

function sayHi(e) {
    console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

3.实现一个once函数,传入函数只执行一次。

// 实际也是一个防抖,利用闭包原理
function once(fn){
    var flag = true
    return function(){
        if(flag){
            fn.apply(null,arguments)
            flag = false
        }
    }
}

4.将原生ajax封装为promise

这里考察原生ajax知识点和promise用法。

var myAjax = (url,type="GET",data={})=>{
	let params = (d) => {
		let arr = []
		for(let prop in d){
			arr.push(prop + "=" + d[prop])
		}
		return arr.join("&")
	}
	
	return new Promise((resolve,reject)=>{
		let xhr = new XMLHttpRequest()
		let isGet = type.toLowerCase()=="get"
		let sendData = isGet?null:data
		let _url = isGet?`${url}?${params(data)}`:url
		xhr.open(type,_url,true)	//第三个参数为可选,true为异步,false为同步模式。默认true
		xhr.send(sendData)
		xhr.onreadystatechange = () =>{
			if(xhr.readyState==4){
				if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
					resolve(JSON.parse(xhr.responseText))
				}else{
					reject(xhr.status+"请求失败")
				}
			}
		}
	})
}

5.js监听对象属性改变

① 在ES5中可以使用Object.difineProperty来实现已有属性的监听。

var Book = {name:"钢铁是怎样炼成的"}

// 劫持
objProxy(Book,"name")

// 闭包封装
function objProxy(obj,key){
    var val = obj[key]
    Object.defineProperty(obj, key, {
        set: function (newVal) {
            val = newVal
            console.log('你取了一个书名叫做' + newVal)
        },
        get: function () {
            return '《' + val + '》'
        }
    })
}

// 测试
console.log(Book); // {}
console.log(Book.name); // "《钢铁是怎样炼成的》"
Book.name = "三国演义";  //你取了一个书名叫做三国演义
console.log(Book.name); // "《三国演义》"

② 在ES6中使用Proxy进行

var Book = {name:"钢铁是怎样炼成的"}
var proxyBook = new Proxy(Book, {
	set: function (target, prop, newVal) {
	    target[prop] = newVal
	    console.log('你取了一个书名叫做' + newVal)
	},
	get: function (target, prop) {
	    return '《' + target[prop] + '》'
	}
})

//测试
console.log(proxyBook);	//Proxy {name: "钢铁是怎样炼成的"}
console.log(proxyBook.name); //"《钢铁是怎样炼成的》"
proxyBook.name = "水许传"; //你取了一个书名叫做水许传
proxyBook.price = 68;	//你取了一个书名叫做68
console.log(proxyBook);	//Proxy {name: "水浒传", price: 68}

6.用setTimeout实现setInterval

使用setInterval在处理某些重复事件(如果定时器代码在代码再次添加到任务队列之前还没执行完成,就会导致定时器代码连续运行好几次而之间没有间隔)时会有两个问题:①某些间隔会被跳过;②多个定时器代码执行时间可能会比预期小。

如下使用setTimeout取代setInterval

// 递归思路
function myInterval(){
	//do something
	setTimeout(myInterval,200)
}
setTimeout(myInterval,200)

// 或者
// arguments.callee代表当前函数,多用于递归调用
setTimeout(function(){
	//do something
	setTimeout(arguments.callee,200)
},200)

7.sleep实现

循环阻塞:传统思路实现sleep效果,需要将程序阻塞。而阻塞一般使用while方式。这种方式性能较差,且容易造成死循环。

function sleep(delay){
	var nextTime = Date.now() + delay
	while(Date.now() < nextTime);
        return true
}
// 测试
sleep(5000);
console.log(1)

async/await方法:这种方法需要与promise和定时器搭配。但这种方法必须封装到async函数中,若将async放到上下文中,同样是异步执行。

function sleep(delay){
	return new Promise((resolve)=>{
		setTimeout(()=>{
			resolve(true)
		},delay)
	})
}
(async function(){
	await sleep(5000)
	console.log(2)
})()

8.写一个函数,第一秒打印1,第二秒打印2

使用let块级作用域+延时器

for(let i=1;i<5;i++){
    setTimeout(()=>{
        console.log(i)
    },1000*i)
}

使用闭包+延时器

for(var i=1;i<5;i++){
    (function(i){
        setTimeout(()=>{
            console.log(i)
        },1000*i)
    })(i)
}

使用闭包+setInterval

function fn(){
    var i=1
    var timer = setInterval(()=>{
        console.log(i)
        i++
        i>5 && clearInterval(timer)
    },1000)
}

9.在数组原型链上实现数组去重的方法

Array.prototype.distinct = function(){
	var newArr = []
	for(var i=0;i<this.length;i++){
		if(newArr.indexOf(this[i]) < 0){
			newArr.push(this[i])
		}
	}
	return newArr
}

// 由于for...in遍历数组时,还会遍历原型上的属性和方法
// 所以在自定义数组原型方法时有必要将其设为不可枚举(原生的方法都不可枚举)
Object.defineProperty(aaa.prototype, 'distinct', { enumerable: false } )

10.下划线命名与驼峰命名互转

// 下划线转驼峰
function _toHump(key){
	// 去除首尾的下划线
	// var _key = key.replace(/(^_*)|(_*$)/g,"")
	var splitKey = key.split("_")
	var HumpKey = splitKey.map((item,index)=>{
		if(index>0 && item!=""){
			item = item[0].toUpperCase()+item.slice(1)
		}
		return item
	})
	return HumpKey.join("")
}

// 驼峰转下划线
function hump2_(key){
	var splitKey = key.split("")
	var HumpKey = splitKey.map((item,index)=>{
		// 判断是否为大写字母
		if(index>0 && item>="A" && item <= "Z"){
			item = "_" + item
		}
		return item
	})
	return HumpKey.join("")
}

11.页面加载进度实现

思路:利用document.onreadystatechange事件判断页面加载状态。页面加载会触发两次此事件,分别为document.readyState="interactive"(可交互:文档已被解析但资源文件、样式文件和框架未加载)和document.readyState="complete"(文档和所有子资源已完成加载,onload事件即将触发)。complete状态可以将进度赋值100%。中间可以用定时器动态得改变进度得百分比。为避免进度已经100%但文档还未加载完成的情况,可以设定一个等待的值如80%时清除定时器。

!function(){
	var n = 0
	var timer = setInterval(function(){
		if(n==100 || n==80){
			clearInterval(timer)
		}
		n+=10
		console.log(n)
	},50)
	document.onreadystatechange = function(){
		if(document.readyState=="complete"){
			n = 100
			console.log(n)
		}
	}
}()

12.手写parseInt方法

思路:使用隐式转换。如+"10"===1010+""==="10"

function _parseInt(str) {
	let str_type = typeof str;
	
	// 如果类型不是 string 或 number 类型返回NaN
	if (str_type !== 'string' && str_type !== 'number') {
		return NaN
	}
	
	// 如果是数字转为字符串
	str = str_type == 'number' ? str+"" : str

	// 字符串处理
	str = str.trim().split(".")[0]	//取小数点前面的字符
	var strArr = str.split('')	//转为字符串数组
	
	if (!strArr.length) {
		// 如果为空则返回 NaN
		return NaN
	}
	
	var strArrNum = strArr.map(function(str) {
		return +str	//隐式转换为数字
	})
	
	var num = strArrNum.reduce(function(x, y) {
		return x * 10 + y	//将数字数组转为整个数字
	})
	
	return num
}

_parseInt('123') //123
_parseInt('123.9') //123
_parseInt(123) //123
_parseInt(123.9) //123
_parseInt() //NaN
_parseInt("") //NaN
_parseInt("QWQW") //NaN

13.原生实现bind

Function.prototype.mybind = function (context) {
    // 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
    // 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
    context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);

    var args = Array.from(arguments).slice(1)
    context.fn = this

    return function() {
        return context.fn(...args)
    }
}

// 手写call
Function.prototype.myCall = function(context){
	// 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
	// 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
	context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);

	// 这里的this是需要使用call的函数,将其赋给传入的指定this指向的对象,这样这个函数就指向了那个传入的对象
	// 函数中的this指向调用它的对象
	context.fn = this
	const res = context.fn(...[...arguments].slice(1))

	//删除这个挂载的函数
	delete context.fn

	// 将执行结果返回
	return res
}

14.找出数组重复项

思路1:遍历数组每一项,找出其之后的选项中是否有此项

function findRepeat(arr){
    var tempArr = []
    arr.forEach((item,index)=>{
        if(tempArr.indexOf(item)<0  && arr.slice(index+1).indexOf(item)>=0){
            tempArr.push(item)
        }
    })
    return tempArr
}

思路2:利用sort方法,先使用sort方法将数组排序,再来判断找出重复元素

function res(arr){
    var temp=[];
    arr.sort().sort(function(a,b){
        if(temp.indexOf(a)===-1 && a===b){
            temp.push(a)
        }
    })
    return temp;
}

思路2:利用ES6的数组filter方法,通过判断第一个重复项引索与当前引索不同找出重复项,然后再将重复项去重

Array.from(new Set(arr.filter((item,index)=>arr.indexOf(item)!==index)))

15.递归实现扁平数组->树结构

将arr1转为arr2

var arr1 = [
	{id:1,pid:0},
	{id:2,pid:1},
	{id:3,pid:1},
	{id:4,pid:3}
]

var arr2 = [
	{
		id:1,
		pid:0,
		children:[
			{
				id:2,
				pid:1,
				children:[]
			},
			{
				id:3,
				pid:1,
				children:[
					{
						id:4,
						pid:3,
						children:[]
					}
				]
			},
		]
	}
]

思路:递归将children作为参数进行运算

function fn(arr){
	let res = arr.filter(obj=>obj.pid===0)
	let doChildren = function(childArr){
		childArr.forEach(item=>{
			item.children = arr.filter(obj=>obj.pid===item.id)
			if(item.children.length){
				doChildren(item.children)
			}
		})
	}
	doChildren(res)
	return res
}

// 最简方案
function formatToTree(ary, pid) {
        return ary
          .filter((item) =>
            // 如果没有父id(第一次递归的时候)将所有父级查询出来
            // 这里认为 item.parentId === 1 就是最顶层 需要根据业务调整
            pid === undefined ? item.pid === 0 : item.pid === pid
          )
          .map((item) => {
            // 通过父节点ID查询所有子节点
            item.children = formatToTree(ary, item.id);
            return item;
          });
}

16.数组扁平化

数组扁平化是指将一个多维数组变为一维数组。

思路1递归:reduce遍历数组每一项,若值为数组则递归遍历,否则concat

function flatten(arr) {  
    return arr.reduce((result, item)=> {
        return result.concat(Array.isArray(item) ? flatten(item) : item);
    }, []);
}

思路2:toString 将多维数组变为字符串然后再用split分割还原为数组

arr.toString().split(',')

思路3:join也可以将多维数组变为字符串然后再用split分割还原为数组

arr.join().split(',')

思路3:普通递归法,这里略

思路4:扩展运算符将多维数组拆开为单个元素,然后利用concat传多个值

[].concat(1,2,3) //[1,2,3]
[].concat(...["1","2",["3","4"]]) //[1,2,3,4]

17.两个数组判断相等

两个数组是没办法直接判断是否相等的,arr1==arr2结果必然是false,因为数组是引用类型,内存地址不同。

若要判断数组相等与否,要考虑数组元素的顺序(数组是有序的,如果不需要考虑顺序,可以不用排序),数组元素的类型(如果是嵌套数组还要使用递归)。

这里我们不考虑数组元素为不同引用的对象问题,这里默认如果其中两个对象引用地址不同则直接为不等。

数组的扩展方法最好是放到数组原型上,方便直接继承。

Array.prototype.equals = function (arr) {
    if (!arr) return false;
    if (this.length !== arr.length) return false;
    // 这里使用普通for循环便于配合return
    for (let i = 0; i < this.length; i++){
        let thisItem = this[i], arrItem = arr[i];
        // 同为数组,递归判断
        if (Array.isArray(thisItem) && Array.isArray(arrItem)){
            if (!thisItem.equals(arrItem)) return false;
        }
        // 直接判断是否相等
        else if (thisItem !== arrItem) return false;
    }
    return true
}
// 使用
let aaa = [1,2,[1,2]]
aaa.equals([1,2,[1,2]]) // true

// 由于for...in遍历数组时,还会遍历原型上的属性和方法
// 所以在自定义数组原型方法时有必要将其设为不可枚举(原生的方法都不可枚举)
Object.defineProperty(aaa.prototype, 'equals', { enumerable: false } )

js知识总结—理论知识篇(一)

2019
04/03

1. js 是由哪三部分构成?

  1. ECMAScript(JavaScript语法 )
  2. DOM 文档对象模型
  3. BOM 浏览器对象模型

ECMAScript

ECMAScript 是由ECMA 国际( 原欧洲计算机制造商协会)进行标准化的一门编程语言,这种语言在万维网上应用泛。ECMAScript 规定了JS的编程语法和基础核心知识,是所有浏览器厂商共同遵守的一套JS语法工业标准。

作用:提供JS核心语言功能

DOM —文档对象模型

文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标记语言的标准编程接口。 通过 DOM 提供的接口可以对页面上的各种元素进行操作(大小、位置、颜色等)。DOM 通过创建树来表示文档,从而使开发者对文档的内容和结构进行操作。

作用:提供访问操作网页内容的方法和接口。如果没有 DOM,就不能渲染页面和对页面经行操作了。

BOM —浏览器对象模型

浏览器对象模型(Browser Object Model),不同浏览器提供了可以对浏览器窗口进行访问和操作的方法。

例如:弹出新的浏览器窗口,移动、改变和关闭浏览器窗口,提供详细的网络浏览器信息(navigator object),详细的页面信息(location object),详细的用户屏幕分辨率的信息(screen object),对浏览器存储的的支持等等。

BOM作为JavaScript的一部分并没有相关标准的支持,每一个浏览器都有自己的实现,虽然有一些非事实的标准,但还是给开发者带来一定的麻烦。

作用:提供与浏览器交互的方法和接口。

2.如何理解 JS 中的this关键字?

“this” 一般是表示当前作用域所在的对象。在JS函数中的this关键字由函数的调用者决定,谁调用就this就指向哪个。如果找不到调用者,this将指向windows对象。

3.什么是闭包

什么是闭包:《js高级程序设计》对闭包的定义为,闭包,其实是一种语言特性,它是指的是程序设计语言中,允许将函数看作对象,然后能像在对象中的操作般在函数中定义实例(局部)变量,而这些变量能在函数中保存到函数的实例对象销毁为止,其它代码块能通过某种方式获取这些实例(局部)变量的值并进行应用扩展。

说白一点,闭包,并不是函数,但离不开函数。闭包是一种语言特性。什么特性呢?可以在函数中封装私有变量,私有变量不会污染全局。且私有变量能够通过其他方式(比如将函数作为返回值或者参数,也就是高级函数)让函数外部能够访问到其私有变量。而且,这个私有变量不会被回收。直到这个函数的实例被销毁或者手动销毁此变量。等于说变相延长了变量的生命周期。

如何形成闭包:当一个函数作用域中的变量被另一个函数引用的时候,就已经形成了闭包。

另一种解释:函数中嵌套函数,且外层函数局部变量被子函数访问。就会形成闭包。一个函数的返回值或参数是函数,也算是嵌套函数。

实际上,如果一个函数访问了它的外部变量,那么它就已经形成了闭包。

function add(n){
  var num = n
  return function addTo(x){
    return x + num
  }
}
addTwo = add(2)
addTwo(5)

内存:一个不能被回收的栈内存就是闭包。

作用:

  1. 封装私有变量或方法。在JS中没有明确的方法来创建私有方法,但是闭包可以。
  2. 外部想要访问函数内部变量,可以使用闭包。可以像访问全局变量一样去使用局部变量,延长了变量的生命周期。
  3. 可以创建不同的实例,且互不干扰。类似于面向对象。比如Vue组件中的deta使用函数和返回值定义数据。实际上就是工厂模式的对象。互不干扰。再比如Vue3和react中的hooks写法。

优点:

  1. 可以读取函数内部变量。
  2. 延长变量生命周期。
  3. 避免全局变量的污染

缺点:

  1. 闭包会常驻内存,增加内存消耗。
  2. 不恰当的闭包有可能会造成内存泄漏。

闭包有三个特性:

  1. 函数嵌套函数
  2. 函数内部引用了外部的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

延伸:

  1. js作用域
  2. js作用域链
  3. js栈内存和堆内存
  4. js垃圾回收机制
  5. js中真的有堆和栈吗

4.js的作用域

作用域就是变量的使用范围,也就是在代码的哪些部分可以访问这个变量,哪些部分无法访问到这个变量,换句话说就是这个变量在程序的哪些区域可见。

作用域就是代码的执行环境,作用域存在于栈内存。

原生js(不含ES6)没有块级作用域只有局部作用域(函数内部)和全局作用域。

一般认为的作用域是词法作用域(也就是静态作用域,如函数的作用域在函数定义的时候就确定了),常见的有函数作用域(如果在函数内部给一个未定义的变量赋值,这个变量会转变为一个全局变量)和块作用域(块作用域把标识符限制在{}中)

作用域链:多个作用域对象连续引用形成的链式结构。一般情况下,变量在当前作用域中取值。但是如果在当前作用域中没有查到值,就会向上级(由下到上,由内而外)作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。注意:只有函数才能形成作用域。

ES6在有了let以后,拥有了块级作用域。但ES6的块级作用域并不是真正意义上的块级作用域。而在浏览器底层是用函数模拟的。也就是说,它的块级也是函数作用域(脚本作用域)。

//除函数外,普通的语句{}不会形成作用域,仍然属于全局。故如下代码可以打印if语句内申明的变量
var a = true;
if(a){var b = 222};
console.log(b); //222

//等同于如下,但建议用下面方式写,语义化更好
var a = true,b;
if(a){b = 222};
console.log(b); //222

函数会形成局部作用域。局部作用域内的变量申明不会污染外部作用域。

//函数和全局作用域
var a=100 //定义全局作用域变量
function fn(){
    var a=200  //在函数中定义变量不会污染外面定义的变量,只能函数里面用
    console.log('fn', a) 
}
console.log('1', a)  //1 100
fn() //fn 200

改变函数作用域的方法:

  1. eval()方法接收一个字符串作为参数,这个字符串的作用域与上下文同域。
  2. with关键字,是重复引用同一个对象的多个属性的快捷方式。

5.解释jsonp的原理,以及为什么不是真正的ajax?

原理:Jsonp就是利用浏览器可以动态地插入一段js并执行;

ajax的核心是 : 通过XMLHttpRequest(异步HTTP请求对象)获取非本页内容,

jsonp的核心 : 动态添加<script>标签来调用外部js脚本。

jsonp只支持get请求,ajax支持get和post请求

jsonp要用json格式传递数据,而ajax不一定用json格式,可能是字符串、数组等。

6.事件绑定和普通事件有什么区别?

事件绑定就是将事件通过类似于addEventListener的方法绑定在dom元素上;

普通事件即为dom元素直接调用事件方法;

普通事件可以被第二次掉用的事件覆盖,而事件绑定不会被覆盖,而是会依次执行。

DOM之事件详解参考以下文章:

DOM之事件触发、冒泡、捕获详解

DOM之事件定义-Event类

7.js事件流(事件捕获、事件冒泡)

事件流:事件流是描述从页面中接收事件的顺序。也可以说是事件传播的顺序。

“DOM2事件流”规定的事件流包括三个阶段:

  1. 事件捕获阶段:提供了在事件没有送达目标之前查看事件的机会。与冒泡方向相反,由父向子逐级捕获。
  2. 处于目标阶段:目标对象(元素)本身的时间处理程序调用(执行)。
  3. 事件冒泡阶段。目标元素的事件大部分都会冒泡(传播)到DOM树根。有一些特殊的事件不会冒泡,比如mouseenter和mouseleave事件不会冒泡。若要冒泡,可以使用mouseover和mouseout替代。

addEventListener

事件处理程序的注册方法为addEventListener(),它接受3个参数(事件类型,事件回调函数,布尔值),其中最后一个参数默认值为false,将会把程序注册为冒泡事件处理程序。而其若为true,则为捕获事件处理程序。两者的区别是:若为冒泡,表示在冒泡阶段调用事件处理程序,逐渐冒泡到外层。而捕获恰好相反,在捕获阶段调用处理程序,执行顺序相反。在IE中只支持事件冒泡。移除事件使用removeEventListener(),传参如上。

IE8及以下浏览器不支持此方法,使用attachEvent(on+事件类型, 事件回调函数)方法,只有两个参数(不支持事件捕获)。移除事件使用detachEvent()方法,参数相同。

window.onclick

注册事件最简单的方法就是直接给元素加事件属性(DOM0级模型),将处理程序赋值给相应的事件属性(事件前面带on)。这种方式适用于所有浏览器。但是缺点是元素将最多只有一个事件处理程序。

如何实现先冒泡后捕获的效果

在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一事件,监听捕获和冒泡,分别对应不同的处理函数,监听到捕获事件,先暂缓执行其处理函数,直到冒泡事件被捕获后再执行捕获事件处理函数。

8.事件模型

js包含三种事件模型:DOM0事件模型(始事件模型),DOM2事件模型,IE事件模型

  1. DOM0级模型:事件不会流动传播。事件绑定监听函数方式:① html标签中添加属性如<div onclick="fun()"></div>;② js中获取dom元素后给元素加事件属性:document.onclick = fn,取消监听使用document.onclick = null
  2. IE事件模型:使用attachEvent("onclick", handler)detachEvent("onclick", handler)方法进行绑定事件和移除绑定。事件流只有2个阶段,没有事件捕获阶段。
  3. DOM2级模型:addEventListener()removeEventListener()进行监听和取消。事件流有3个阶段。

9.js事件委托

事件委托指的是,不在事件的目标对象(元素)上设置监听函数,而是在其父元素上设置监听函数。通过事件冒泡,父元素可以监听到子元素上事件的触发。通过判断事件目标元素的类型来做不同的操作。jQuery的on方法就是使用了事件委托。事件委托常用与动态元素的绑定,新添加的子元素也可以有监听函数,也可以触发事件回调。

举例:比如我们要实时监听li元素的事件,可以将事件注册到ul标签上。这样当li元素动态添加后也能捕获到添加的li元素事件。

10.js垃圾回收机制(GC)

js解释器当监测到一个对象(字符串、数组等)无用的时候,为了避免内存占用,就会把这一块的内存释放掉。这就是垃圾回收。GC执行时遍历所有对象,对不可访问的对象进行回收。

垃圾回收的方法是:

  1. 清除标记:垃圾回收器会给内存中的变量添加标记。当进入环境时标记“进入”,离开环境时标记“离开”。回收器会适时删除离开环境中的变量(当然被引用的变量不会被删除)。
  2. 引用计数:为每个引用类型的变量的引用次数计数,当被另一个变量引用时记为1,当引用其他变量时,这个变量计数-1,当回收器再次执行时就会清除计数为0的值。但如果相互引用的次数过多(循环引用),就不会触发回收器,造成内存泄漏。
  3. 若要回收一个变量,可以将变量手动赋值为null。

为什么需要垃圾回收?

由于字符串、对象、数组等数据没有固定的大小,只有当他们的大小已知时,才能对他们进行存储和分配。每次创建变量时js解释器都会分配内存来存储这个实体。如果内存不够用,将会造成内存溢出导致系统崩溃。所以必须要有一套内存的释放机制。这就是垃圾回收机制的必要性。

var a = "hello world"
var b = "hello"
var a = b //此时会释放掉"hello world"

11.js内存之堆和栈

堆(Heap)和栈(Stack)是程序内存的两种管理方式。其也是两种不同的数据结构。

栈内存:系统自动分配释放 ,用于存放基本类型的变量和对象的引用变量。栈内存存取速度比堆要快很多(栈使用一级缓存,堆使用二级缓存),仅次于寄存器(寄存器是cpu的内存单元)。满足先进后出的数据结构,栈数据可以共享。

堆内存:允许程序在运行时动态申请某个大小的内存空间。由开发人员分配和释放。结构类似于链表。相当于一棵树。

基本类型:String,Number,Boolean,Null,Undefined,symbol,这6种基本数据类型它们是直接按值存放的,所以可以直接访问。存于栈内存中。

引用类型:Function,Array,Object,当我们需要访问这三种引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

函数被声明后在堆中以字符串形式存储,且申明函数作用域(脚本作用域)。被调用时在栈中开辟执行环境栈,作用是执行代码,而非保存代码。函数作用域(脚本作用域)中的变量在申明后会与全局区分开来,保障每个词法作用域的代码独立性。

内存溢出:

如果想要堆溢出,比较简单,可以循环创建对象或大的对象; 如果想要栈溢出,可以递归调用方法,这样随着栈深度的增加,JVM(虚拟机)维持着一条长长的方法调用轨迹(作用域链),直到内存不够分配,产生栈溢出。

相关知识点:深拷贝和浅拷贝

12.两个script标签中的代码,一处报错,另一处有影响吗?

两个script标签是两个代码块。报错不会影响另一处。但如果后面的代码块有引用前面的变量或对象,是会出错。

Vue重点知识总结—基础篇(一)

2018
05/10

1.Vue 中怎么自定义过滤器

可以用全局方法 Vue.filter() 注册一个自定义过滤器,它接收两个参数:过滤器 ID 和过滤器函数。过滤器函数以值为参数,返回转换后的值

Vue.filter('reverse', function (value) { 
    return value.split('').reverse().join('') 
})
<!-- 'abc' => 'cba' --> 
<span v-text="message | reverse"></span>

2.Vue 中怎么自定义指令

全局注册

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

局部注册

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

3.$route和$router的区别

$router 为 VueRouter 实例,想要导航到不同 URL,则使用 $router.push 方法$route 为当前 router 跳转对象里面可以获取 name 、 path 、 query 、 params 等

4.vue-router 使用params与query传参有什么区别

vue-router 可以通过 params 与 query 进行传参

// 传递
this.$router.push({path: './xxx', params: {xx:xxx}})
this.$router.push({path: './xxx', query: {xx:xxx}})

// 接收
this.$route.params
this.$route.query

params 是路由的一部分,必须要有。query 是拼接在 url 后面的参数,没有也可以。
params 不设置的时候,刷新页面或者返回参数会丢,query 则不会有这个问题。

5.vm.$mount()作用,$mount 和 el的区别

$mount和实例化Vue的el参数作用相同,是用来手动执行挂载到相应的dom元素。底层用的是querySelector()方法。若$mount不传参,则模板将被渲染为文档之外的的元素,并且你必须使用原生DOM API把实例的$el插入文档中(注意:当前vue实例或组件实例必须有template模板,否则$el为空),这样也可以渲染模板中的数据。

若使用script引入vue这种方式进行开发,非组件化开发,一般每个页面都会实例化一个新的vue实例。这时就不需要在实例中添加template模板,因为针对的是整个DOM。这时实例化时必须有el标签或调用$mount("dom")方法,且$mount方法必须传参,此时Vue会通过 el选项或$mount指定的挂载元素中提取出innerHtml作为其template模板编译渲染函数。否则数据或方法将无法挂载(vue不知道挂载到哪个元素上,使用appendChild挂载将无效,因为没有template)。

el和$mount区别

两者在使用效果上没有任何区别,都是为了将实例化后的vue挂载到指定的dom元素中。

如果在实例化vue的时候指定el,则该vue将会渲染在此el对应的dom中,反之,若没有指定el,则vue实例会处于一种“未挂载”的状态,此时可以通过$mount手动执行挂载

// 扩展组件
var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')

// 同上
new MyComponent({ el: '#app' })

// 实例化Vue
new Vue({ el: '#app' })

// 同上
new Vue({}).$mount('#app')

// 或者,在文档之外渲染并且随后挂载
// 注意:挂载必须使用appendChild方法,若使用innerHtml是无法挂载的,因为$el是一个dom对象,不是标签字符串
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

6.实例化Vue中的render参数作用(render:h=>h(App))

若使用script引入vue这种方式进行开发,实例化时并不需要render参数。因为没有组件template,此时Vue会通过 el选项或$mount指定的挂载元素中提取出innerHtml作为其template模板编译渲染函数。

若使用组件化开发,实例化vue时实际上需要一个外层template(比如app.vue),vue需要把这个模板先放到页面的某个标签内,再将数据挂载到这个标签。所以render方法就相当于组件中的templatecompoments两个参数的组合。只不过在组件实例中templatecompoments方式比render优先级高一些。而在vue实例中render优先级更高。

import Vue from 'vue';
import VueRouter from "vue-router";
import App from './App';
const app=new Vue({
    el:'#app',
    router,
    render:h => h(App) //App实际上就是一个组件
});

// 上述代码作用等同于
const app=new Vue({
    el:'#app',
    router,
    compoments:{
        App
    },
    template:"<App/>"
});

// 组件
var A = Vue.component("A",{
	template:"<div>{{notice}}</div>",
	data(){
		return{
			notice:"21212112"
		}
	}
})
const app=new Vue({
    el:'#app',
    render:h => h(A)
});

Vue 在创建 Vue 实例时,通过调用 render 方法来渲染实例的 DOM 树。Vue 在调用 render 方法时,会传入一个 createElement 函数作为参数,也就是这里的 h 的实参是 createElement 函数,然后 createElement 以App为参数进行调用,生成一个 VNode节点。render 函数得到这个VNode节点之后,返回给 Vue.jsmount 函数,渲染成真实 DOM 节点,并挂载到根节点上。

vue将createElement简化为h,因为它来自单词 hyperscript,这个单词通常用在虚拟dom的实现中。Hyperscript 本身是指生成HTML 结构的 script 脚本。

render:function(createElement){
    return creatElemnt(App);
}

7.Ajax请求放在哪个生命周期?

Ajax可以放到created及其之后的生命周期。

  1. created时候,DOM未挂载,这里请求的数据可以存到data中。但是无法操作dom,没办法将数据渲染到页面。
  2. mounted时DOM已挂载完成,这里可以操作DOM节点。一般情况下都放到mounted中。
  3. 生命周期时同步进行的,Ajax是异步进行的。为保证逻辑统一性,尽量将Ajax都放到同一个生命周期中。
  4. 服务端渲染时(SSR)不支持mounted(无DOM),所以放到created中。

8.何时需要使用beforeDestroy?

实例销毁之前钩子。在这里实例仍然可用。

  1. 可以解绑$on方法。使用$off方法。(在非父子组件通信时会借助中间Vue实例,然后使用vm.$emit()vm.$on()方法进行通信)
  2. 可以清除定时器。
  3. 可以移除绑定的事件监听。使用removeEventListener

9.Vue父子组件的生命周期调用顺序

渲染调用顺序:先父后子;渲染完成顺序:先子后父

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 若还有嵌套子组件,则将子mounted缓存,继续下级子组件渲染 -> 父mounted

更新操作:先父后子;更新完成顺序:先子后父

父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

销毁操作:先父后子;销毁完成顺序:先子后父

父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

10.vue组件如何通信

父子通信:子组件通过 props 属性,绑定父组件数据,实现双方通信

子父通信:由于Vue数据传递是单向数据流(儿子不能直接改父亲)

  1. 事件监听方法:父组件通过$on监听,子组件 $emit 触发。(简易的发布-订阅)
  2. 父组件通过props给子组件传递一个方法,子组件调用这个方法。

其他通信方式:

  1. 通过$children $parent获取父子组件的实例来拿到父子相应的数据。
  2. 通过provideinject在父组件中提供数据,子组件进行注入(写插件使用,把provide挂到$options上,内部会挂到实例上vm._provided,子组件会直接将其挂到数据中,而且是响应式的)。
  3. 通过ref获取组件的实例,从而拿到组件的数据。(普通标签获取的事dom元素,组件标签获取的是组件实例)

非兄弟组件通信:

  1. 在组件结构相对简单情况下,可以通过兄弟组件中的父组件, 子组件先把数据传递给父组件,  父组件再传递给另外的子组件。
  2. Event Bus 实现跨组件通信(公共的vue实例进行通信),如 Vue.prototype.$bus = new Vue()
  3. 使用状态管理插件vuex
//使用中间vue实例实现子-子通信

//mid
Vue.prototype.$bus = new Vue()

//某子组件发消息
//....
methods: {
    tellname () {
       // 发出事件,传递数据,givename自定义事件
     this.$bus.$emit('givename', this.mybfname) 
  } 
}

//某子组件收消息

// 组件一加载就进行兄弟组件所发出的事件的监听
created () {
    // vm.$on(vue自定义的事件, 处理函数)
    // 处理函数有一个默认参数,就是其它组件所传递的数据
    this.$bus.$on('givename', (data) => {
      console.log(data);
      this.mysbfname = data
    })
}

11.Vue相同逻辑如何抽离

Vue.mixin方法。可以给组件每个生命周期和函数混入一些公共逻辑。

12. Vue中如何实现⼦组件内的css样式名在项⽬中绝对唯⼀性

style标签上加上scoped属性

13. 说出⾄少4种Vue当中的指令和它的⽤法?

  1. v-if(判断是否隐藏,⽤来判断元素是否创建)
  2. v-show(元素的显⽰隐藏,类似css中的display的block和hidden)
  3. v-for(把数据遍历出来)
  4. v-bind(绑定属性、响应式数据)
  5. v-model(实现双向绑定、语法糖)

Vue重点知识总结—基础篇(二)

2018
05/10

1.Vue 组件 data 为什么必须是函数

因为js本身的特性带来的,Vue组件可能会有多个实例,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到当前组件所有实例的数据。如果将 data 作为一个工厂函数,那么每一个组件实例的 data 属性都是全新独立的,不会相互影响了,有效规避了很多组件状态污染问题。

通过查看源码发现,在state.js的initData方法中,对data的类型做了判断,如果是函数类型,则执行getData(data.call()直接执行)方法获取其值,如果是普通对象则返回原对象。

在组件中使用对象作为data的值,在编译时会直接报错。

原理:

  1. data是被挂到组件类的原型中,如果不是函数而直接是一个对象,会被其他任何组件继承,任何组件都可以对其进行修改。
  2. 如果data是一个工厂对象,每个组件进行调用时,执行函数都会返回一个全新的对象,就可以避免组件之间的数据互相影响。
  3. Vue实例化根组件时data是一个对象,是因为Vue根实例只会实例化一次,是单例的,实例不会被公用。不用考虑组件之间干扰问题。

2.计算属性computed 和 methods 有什么区别

我们可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的。

区别:

  1. computed计算属性是一个watcher有缓存,计算属性是基于依赖(依赖就是data中的数据)进行缓存的。只要依赖的数据不发生改变,计算属性中的方法是不需要执行的,一直使用缓存中的数据。当它的相关依赖发生改变时会重新赋值。当多次调用的时候只要依赖项不改变,就不会重新执行,从而节省了内存。(缓存机制在源码中通过dirty状态去判定)
  2. method 就是普通的函数。不依赖data中的数据,也没有缓存,每次调用都重新执行一遍。
  3. 函数的调用时需要加()的,而计算属性调用不需要加括号。
  4. 计算属性是必须有返回值的。

3.侦听器 watch和计算属性computed区别

两者的原理相同,在内部都是watcher,主要区别就是computed是有缓存的。如下:

  1. 侦听器 watch(数据一旦发生变化就通知侦听器所绑定的方法)。watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法;
  2. 计算属性computed是根据一个现有数据去生成一个新数据,并且这两个数据会永久的建立关系,还会建立缓存,当数据没有改变的时候,不会重新计算而是直接使用缓存中的值。
  3. 初始化computed计算属性时会实例化一个watcher,这个watcher有一个lazy:true标识,代表计算属性。还有一个默认dirty:true的标识,为true时才会让计算属性执行。当第一次取值时会执行,执行完毕后会将dirty改为false,也就是说下次再取值不会执行,会直接使用缓存的值。只有当计算属性所依赖的属性发生变化时,会将dirty改为true
  4. computed的底层还是使用Object.defindProperty来实现视图更新。
  5. watch一般用于异步、定时任务、或者开销较大(耗时)的操作。
  6. watch 中的属性 一定是data 中 已经存在的数据。
  7. 当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,此时就需要deep属性对对象进行深度监听(深度遍历和递归)。
  8. 使用 watch 选项允许我们执行异步操作,并在我们得到最终结果前,进行中间操作。也可以在数据频繁变化时进行自定义的操作如过滤操作。 而计算属性无法做到的。
  9. 计算属性是必须有返回值的。
watch:{
    obj:{ //监听的对象
        deep:true, //深度监听设置为 true
        handler:function(newV,oldV){
            console.log('watch中:',newV)
        }
    }
}
// deep:true的实现就是深度遍历和递归,很耗性能。
// 如果监听的是数组,deep:true会将数组中的每一项进行遍历取值,会将当前的watcher存到对应属性或数组项的依赖中,实现深层数据变化也会通知。

4.v-if 和 v-show 区别

使用了 v-if 控制页面元素节点是否渲染。v-show 改变 CSS 中的 display 显隐。故想要频繁控制显隐,最好使用v-show

两者不能连用,虽然连用不会报错,但是只有两者同时为true时才会显示节点,无法进行精准的判断。而且两者连用会在渲染函数中做两次判断,影响了性能。

底层原理:

  1. v-if在内部使用with语法,使用三元运算符判断v-if的值的真假。若为真,则渲染节点。若为假,则不渲染。
  2. v-show在内部使用创建指令方式,判断值为假,则改变节点样式display:none,若为真,则使用原有样式。

5.为什么v-if和v-for不能连用?

因为v-forv-if优先级高(vue2.x)。

通过测试得出,如果连用,在渲染函数($options.render)中v-for遍历出的每个元素中都添加v-if指令,性能很差。

所以一般在外层template标签添加v-if,这样如果为false,则不会执行v-for遍历,节省了性能开销。

优化:若非整体显隐,而是在列表中某些字内容有v-if的判断,这时也可以做一个计算属性,通过计算将不需要显示字内容进行过滤,节省了性能。

6.v-for中为什么要有key属性?

在Vue中使用v-for进行列表渲染的时候,它会默认使用“就地更新”的策略。直接对已有的标签就地更新,并不会将所有的标签全部重新删除和重建,只会重新渲染数据,然后再创建新的元素直到数据渲染完为止。

v-for渲染的列表更新时,diff算法会比较新旧节点的标签名key。若进行增加删除列表内节点后,key值会发生变化,那么新旧节点在进行比对时容易错位。最终导致渲染出错。

  1. key主要作用是为了高效得更新虚拟DOM。原理是VUE在patch过程中会通过key精准判断两个节点是否是同一个。从而避免频繁更新不同元素,使得整个patch过程更加高效,减少dom操作量,提高性能。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
  2. 若不设置key,会在列表的渲染过程中发生一些隐蔽的bug
  3. 重复的 key 会造成渲染错误。

没有key情况还会导致节点的删除和重建,影响性能。若有key,在进行更新时会直接保留原有节点,只插入新节点,无需删除重建。

  1. v-for循环的时候,key属性只能使用number/string
  2. key属性值必须是唯一标识符。
  3. key属性不要使用索引作为标识。

用引索index作为key可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
  2. 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
  3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

7.vue的生命周期介绍

Vue实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载DOM->渲染、更新->渲染、卸载等一系列过程,我们称这是Vue的生命周期。

1.vue生命周期的作用是什么?

它的生命周期中有多个事件钩子,让我们在控制整个vue实例的过程时更容易形成好的逻辑。

在于Vue渲染过程的每个节点给一个通知(事件、回调),便于我们在某一个节点进行一些相关的操作。

2.Vue生命周期总共有几个阶段?

它可以总共分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/销毁后

3.第一次页面加载会触发那几个钩子?

第一次页面加载时会触发beforeCreate,created,beforeMount,mounted

4.DOM渲染在哪个周期中就已经完成?

DOM渲染在mounted中就已经完成了

5.生命周期的调用流程:

  1. beforeCreate:实例初始化完成,数据观测(data observer)之前。在这里拿不到实例中的数据。
  2. created:实例创建完成(数据观测完成、计算属性运算完成、method方法绑定、watch和event事件回调绑定),但dom还未挂载,数据还未渲染到页面,拿不到$el(拿不到dom元素)。<create 和 mount 的区别>
  3. beforeMounted:实例挂载之前执行。会调用render函数(调用template)。这个钩子基本用不到。
  4. mounted:实例挂载完成。创建新的vm.$el,把老的el对象删掉。这里可以拿到dom元素。
  5. beforeUpdate:数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。
  6. updated:当前生命周期更新之后调用。由于数据更改,虚拟DOM渲染完成之后。
  7. beforeDestroy: 实例销毁之前调用。在这时,实例仍然可用。
  8. destroyed:实例销毁之后调用。调用后,实例所有数据监听都会解绑,所有事件监听都会移除,所有子实例也会被销毁。

6.生命周期钩子的一些使用方法(重点):

  1. beforeCreate:可以加个loading事件,在加载实例时候触发。
  2. created:在这里可以结束loading事件,执行异步请求将数据存如data。
  3. mounted:获取到dom节点,可以进行一些dom操作。由于这里能够取到dom,也可以在这里执行异步请求。
  4. beforeUpdate:在这里可以进一步更新状态,不会导致DOM重新渲染。
  5. updated:这里尽量不要再更新状态,否则可能会导致死循环。这里一般用不到。就算用到也是执行一些基于dom的操作,如动画效果。
  6. beforeDestroy:可以执行一些优化操作。如清空定时器,移除绑定事件等。
  7. nextTick:更新数据后立即操作dom,回调可以做一些在dom渲染完成后做的事。

8.父子组件执行顺序

在组件开始生成到结束生成的过程中,如果该组件还包含子组件,则自己开始生成后,要让所有的子组件也开始生成,然后自己就等着,直到所有的子组件生成完毕,自己再结束。所以上图中。“父亲”先开始自己的created,然后“儿子”开始自己的created和mounted,最后“父亲”再执行自己的mounted。

9.v-html指令会造成哪些问题?

1.可能会导致xss攻击。

2.会将标签内的子元素全部替换。

10.对 keep-alive 的了解

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存。缓存组件数据。组件切换时不会对其进行卸载。可以使被包含的组件保留状态,避免重新渲染。

keep-alive会改变组件的生命周期。当引入keep-alive的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。

  1. 有两个属性props:include和exclude,想缓存的内容和不想缓存的内容。max属性:最多缓存多少个。
  2. keep-alive内插入多个组件只会缓存第一个。

作用:①提升性能,避免每次重新渲染。(但也不宜太多,太多的话占用内存太大得不偿失)②保留状态(数据)

前端重点知识总结—BOM

2018
12/09

浏览器对象模型(Browser Object Model),不同浏览器提供了可以对浏览器窗口进行访问和操作的方法。

1.常用的Bom属性

① location对象

  1. location.href— 返回或设置当前文档的URL
  2. location.search — 返回URL中的查询字符串部分。例如 http://www.xxx.com/dreamdu.php?id=5&name=dreamdu 返回包括(?)后面的内容?id=5&name=dreamdu
  3. location.hash — 返回URL#后面的内容,如果没有#,返回空
  4. location.host — 返回URL中的域名部分,例如www.xxx.com
  5. location.hostname — 返回URL中的主域名部分,例如xxx.com
  6. location.pathname — 返回URL的域名后的部分。例如 http://www.xxx.com/xhtml/ 返回/xhtml/
  7. location.port — 返回URL中的端口部分。例如 http://www.xxx.com:8080/xhtml/ 返回8080
  8. location.protocol — 返回URL中的协议部分。例如 http://www.xxx.com:8080/xhtml/ 返回(//)前面的内容http:
  9. location.assign — 设置当前文档的URL
  10. location.replace() — 设置当前文档的URL,并且在history对象的地址列表中移除这个URL location.replace(url);
  11. location.reload() — 重载当前页面

② history对象

  1. history.go() — 前进或后退指定的页面数 history.go(num);
  2. history.back() — 后退一页
  3. history.forward() — 前进一页
  4. history.pushState(stateData,nameString,pathUrl) — 把路由页面放入历史记录,无刷新切换路由
  5. history.replaceState(stateData,nameString,pathUrl) — 不放入历史,直接切换

③ Navigator对象

  1. navigator.userAgent — 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
  2. navigator.cookieEnabled — 返回浏览器是否支持(启用)cookie

2.Cookie、sessionStorage、localStorage的区别

  1. Cookie:每次都会在同源的http请求头中携带(即使不需要,过多会带来性能问题),即cookie在浏览器和服务器间来回传递。而sessionStoragelocalStorage不会自动把数据发给服务器,仅在本地保存。
  2. cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下
  3. cookie存储的大小很小只有4K左右,而storage可以存5M甚至更大。
  4. sessionStorage仅在当前浏览器窗口关闭前有效,不可能持久保持;localStorage始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。
  5. localStorage 和cookie在所有同源窗口中都是共享的。而sessionStorage在新标签页总是会初始化一个新的 session。
  6. localStorage可以用来在页面之间传递参数。
  7. 易用性:cookie需要程序员自己封装,原生的cookie使用url query形式存储,开发不友好。

3.浏览器在渲染页面时候会生成哪两棵树?

  1. DOM树:浏览器会遍历服务器返回的HTML文档节点,生成DOM树
  2. CSSOM规则树:浏览器解析CSS样式和文件生成。

4.浏览器缓存机制

浏览器的缓存分为强缓存协商缓存,当客户端请求某个资源的时候,获取缓存的流程如下:

  1. 先根据这个资源的http header(请求头)判断它是否命中强缓存,如果命中,则直接从本地缓存中获取资源,未命中则向服务器请求资源。
  2. 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些请求头验证这个资源是否命中协商缓存,这个过程成为http再验证,如果命中,服务器直接返回请求而不返回资源,告诉客户端从缓存中获取,客户端收到返回后就直接从客户端缓存获取资源。协商缓存返回的状态码一般为304
  3. 强缓存和协商缓存的共同之处在于:如果命中缓存,服务器不会返回资源;区别是:强缓存不发送请求到服务器,但是协商缓存会发送请求到服务器
  4. 当协商缓存没有命中时,服务器会返回资源给客户端。
  5. 当ctrl+F5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存
  6. 当F5刷新页面时,跳过强缓存但会检查协商缓存。
  7. Chrome在解析html时会对a标签的域名进行预解析,然后缓存。

强制刷新ctrl + f5 ,请求头header带有Cache-control:no-cache(为了兼容,还带了 Pragma: no-cache),同时不带有if-not-match / If-Modified-Since,所以请求服务器协商缓存会被当做失效,返回200和最新内容。

关于浏览器缓存的详细内容,包含请求头中携带的相关字段请点此查看

5.浏览器的内核分别是什么?

  1. IE: trident内核
  2. 火狐:gecko内核
  3. Safari:webkit内核
  4. Opera:以前是presto内核,现Blink内核(基于webkit)
  5. Chrome:Blink(基于webkit)

js知识总结–面向对象篇

2018
04/07

JavaScript 的核心是支持面向对象的,同时它也提供了强大灵活的 OOP 语言能力。面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象程序设计的目的是在编程中促进更好的灵活性和可维护性。

1.面向对象三要素

  1. 封装:一种把数据和相关的方法绑定在一起使用的方法。
  2. 继承:一个类可以继承另一个类的特征。如属性或方法。
  3. 多态:顾名思义「多种」「形态」。不同类可以定义相同的方法或属性。就像所有定义在原型属性内部的方法和属性一样,不同的类可以定义具有相同名称的方法;方法是作用于所在的类中。并且这仅在两个类不是父子关系时(两个类是同一个父类的不同子类)成立。

2.面向对象术语

  1. Namespace 命名空间允许开发人员在一个独特,应用相关的名字的名称下捆绑所有功能的容器。 在JavaScript中,命名空间只是另一个包含方法,属性,对象的对象。Javascript中的普通对象和命名空间在语言层面上没有区别。
  2. Class 类定义对象的特征。它是对象的属性和方法的模板定义。
  3. Object 对象类的一个实例。
  4. Property 属性对象的特征,比如颜色。
  5. Method 方法对象的能力,比如行走。
  6. Constructor 构造函数对象初始化的瞬间,被调用的方法。通常它的名字与包含它的类一致。

3.什么是构造函数,和普通函数的区别

函数:用来封装并执行代码块。

构造函数:一种特殊的函数,与关键字new一起使用,是用来生成实例对象的函数。作用是初始化对象, 即为对象赋初始值(添加属性和方法)。

区别:

  1. 构造函数是和new 关键字⼀起使⽤的
  2. 函数用来执行代码块,而构造函数用来创建对象
  3. 普通函数一般遵循小驼峰命名规则,构造函数一般遵循大驼峰命名规则
  4. 调用后返回内容不同,普通函数直接返回return定义的内容,没有return默认为undefined;而构造函数返回this指向的对象

4.什么是原型,什么是原型链?

原型:在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会与之关联一个原型对象。函数天生自带一个prototype属性(对象或构造函数实例自带__proto__属性),这个属性指向函数的原型对象。每一个对象或函数都会从原型中“继承”属性。(只有函数有prototype属性,而对象和数组本身没有。)

让我们用一张图表示构造函数和实例原型之间的关系:

原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。

原型链:对象之间的继承关系,在JS中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条。由相互关联的原型组成的链状结构就是原型链

举例说明:person → Person → Object ,普通人继承人类,人类继承对象类

当我们访问对象的一个属性或方法时,它会先在对象自身(私有属性或方法)中寻找(判断是否为私有属性或方法而非继承来的,使用hasOwnProperty(key)方法),如果有则直接使用,如果没有则会去原型对象中寻找(构造函数的构造属性和方法优先级大于原型上属性和方法),如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则会找到null,返回undefined。null是原型链顶层。

我们可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性;使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true

function Person() {}
Person.prototype.a = 123;
Person.prototype.sayHello = function () {
    alert("hello");
};
var person = new Person()
console.log(person.a) //123
console.log(person.hasOwnProperty('a')) //false
console.log('a'in person) //true
console.log(person.__proto__===Person.prototype)   //true

person实例中没有a这个属性,从 person 对象中找不到 a 属性就会从 person 的原型也就是 person.__proto__,也就是 Person.prototype中查找,很幸运地得到a的值为123。那假如 person.__proto__中也没有该属性,又该如何查找?

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层Object为止。Object是JS中所有对象数据类型的基类(最顶层的类)在Object.prototype上没有__proto__这个属性。

console.log(Object.prototype.__proto__ === null) // true

5.构造函数的原型和实例化对象之间的关系

函数天生自带一个prototype属性,而这个属性指向一个对象(指向函数的原型对象),我们简称原型。

实例化对象会从构造函数的原型中继承其方法和属性,也就是继承prototype对象中的属性和方法。

实例化的对象是怎么指向(怎么继承)的prototype

实例化对象是通过__proto__属性继承的。这个属性指向构造函数的原型对象prototype。换句话说也就是__proto__可以访问prototype里面的所有属性和方法,__proto__prototype是同一个东西,其内存地址相同,__proto__ === prototype // true

prototype作用:

  1. 节约内存
  2. 扩展属性和方法
  3. 可以实现类之间的继承

prototype__proto__、实例化对象三者之间的关系:

  1. 每一个对象都有一个__proto__属性
  2. __proto__指向创建自己的那个构造函数的原型
  3. 对象可以直接访问自己__proto__里面的属性和方法
  4. constructor 指向创建自己的那个构造函数
({}).constructor === Object; // true  
Object.constructor === Function; // true

6.new关键字做了哪些事情(实例化对象或构造函数执行做了哪些事情)

new操作新建了一个对象(实例化构造函数)。这个对象原型链__proto__指向构造函数的prototype,执行构造函数后返回这个对象。构造函数被传入参数被调用,关键字this被指向该实例obj。返回实例obj。

  1. 在内存中开辟了一块空间
  2. 新生成了一个对象
  3. 链接到当前构造函数的原型
  4. 绑定 this,指向当前实例化的对象
  5. 返回这个新对象

隐式操作

开辟新的堆内存,构造函数的执行体this会指向这个堆。代码执行完毕后,会把这个堆内存地址返回并在栈中赋给新的变量。

构造函数执行前,会在函数体最前面添加一个 this = {},然后按照顺序执行代码,然后最后会有 return this; 其中 添加 this={} 以及 return this; 操作是隐式操作,js引擎会自动帮我们执行。

假如强行要改变构造函数返回的结果也是可以的,可以在构造函数里面添加return语句,但是除了{}[]、以及function类型会被如实返回,其他return的情况都会默认返回原来的构造函数的对象。

用函数方法手写一个new

function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

function aaa(a){
    this.a = a
}

new aaa(1) //aaa {a: 1}
create(aaa,1) //aaa {a:1}

function bbb(a){
    return {a:a}
}

new bbb(1) //{a: 1}
create(bbb,1) //{a:1}

7.原生js模拟ES6的类CLASS

//创建类-这个函数也相当于class中的constructor构造函数。
function Person(firstName) {
  this.firstName = firstName; //类的属性,可以用this关键字定义
  alert('Person instantiated');
}

//类的方法(原型链上创建)
Person.prototype.sayHello = function() {
  alert("Hello, I'm " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器,以便在实例化Student时能够执行Person的构造函数
  // 以便继承父类Person的属性
  Person.call(this, firstName);

  // 初始化Student类私有属性
  this.subject = subject;
};

// 建立一个由Person.prototype继承而来的Student.prototype对象.
// 注意: 常见的错误是使用 "new Person()"来建立Student.prototype.
// 这样做的错误之处有很多, 最重要的一点是我们在实例化时
// 不能赋予Person类任何的FirstName参数
// 调用Person的正确位置如下,我们从Student中来调用它
// 用于继承Person的原型链方法
// 这里最好不要直接Student.prototype = Person.prototype,
// 因为prototype是引用类型原因,一旦子类更改相应的方法,父类及其他子类都会继承。
Student.prototype = Object.create(Person.prototype); 
//Student.prototype = new Person(); //或者这样写

// 在不支持Object.create方法的浏览器中可以使用如下方法进行继承
// 这种方式叫做寄生类(中间类)方式,可以砍掉父类的构造函数,避免多次调用。
// 也可以避免直接使用Student.prototype  = Person.prototype造成的单一引用
function createObject(proto) {
    function ctor() { }
    ctor.prototype = proto;
    return new ctor();
}
// Usage:
Student.prototype = createObject(Person.prototype);

// 设置"constructor" 属性指向Student
// js 并不检测子类的 prototype.constructor,我们必须手动申明
// 若不指定,则constructor指向Person
Student.prototype.constructor = Student;

// 更换"sayHello" 方法,这里不会影响父类的sayHello方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

//实例化类,实例化时候构造函数会被立即执行
var person1 = new Person('Alice');//执行alert
var person2 = new Person('Bob'); //执行alert

//类的属性可以在实例化对象后访问
alert('person1 is ' + person1.firstName); // alerts "person1 is Alice"
alert('person2 is ' + person2.firstName); // alerts "person2 is Bob"

//调用类的方法
person1.sayHello(); // alerts "Hello, I'm Alice"
person2.sayHello(); // alerts "Hello, I'm Bob"

封装的体现:Student类虽然不需要知道Person类的walk()方法是如何实现的,但是仍然可以使用这个方法;Student类不需要明确地定义这个方法,除非我们想改变它。 这就叫做封装。

继承的体现:Student类作为 Person类的子类.可以继承父类的所有属性和方法。

多态的体现:我们重定义了sayHello() 方法并添加了 sayGoodBye() 方法.

8.构造函数里的方法和prototype上定义方法的区别

函数内的方法相当于是函数的私有变量,如果新创建的对象需要使用里面的私有变量,就使用函数内的方法。私有变量实现了面向对象的多态性。

在原型链上定义方法一般用于让所有实例和子类继承此方法。体现了面向对象的继承性。

区别:

  1. 定义在构造函数内部的方法,会在它的每一个实例上都克隆(重新定义)这个方法,消耗的内存更高。
  2. 定义在prototype 属性上的方法会让它的所有实例和子类都共享这个方法,不会重新复制。原型链上的方法指向一个内存引索,所有实例都是直接引用,更节省内存。
  3. 使用prototype的方法在继承的时候是共有的,多个实例或继承对象共有这个方法,所以一旦改变,所有的都改变。
  4. 在构造函数中定义的属性和方法要比原型prototype中定义的属性和方法优先级高,如果定义了同名称的属性和方法,构造函数中的将会覆盖原型中的。
  5. 定义在构造函数中的属性会被序列化,而prototype中的属性不会被序列化。所以使用工厂模式定义对象的时候不能用原型prototype定义属性。

9.改变函数内部this指针的方法有哪些,区别是什么?

call,apply,bind三个方法。他们的第一个参数都是要改变的this指向。第二个参数是传给当前函数的参数。

这三个方法的作用是:①改变this指向;②借用别的对象的方法;③调用函数立即执行如call和apply。他们的区别如下:

  1. call:第二个参数是普通函数参数类型,可以有多个参数,使用call方法会立即执行当前函数。如Array.slice.call(this,arg1,arg2,arg3)
  2. apply:第二个参数是一个数组类型。apply只有两个参数。使用apply方法会立即执行当前函数。
  3. bind:第二个参数与call一致。bind方法不会立即执行当前函数,而是返回一个新的函数,需要再次调用(柯里化函数)。
  4. call()性能更佳。

手写call

Function.prototype.myCall = function(context){
	// 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
	// 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
	context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);

	// 这里的this是需要使用call的函数,将其赋给传入的指定this指向的对象,这样这个函数就指向了那个传入的对象
	// 函数中的this指向调用它的对象
	context.fn = this
	const res = context.fn(...[...arguments].slice(1))

	//删除这个挂载的函数
	delete context.fn

	// 将执行结果返回
	return res
}

10.单继承和多继承

单继承:一个子类只有一个父类

多继承:一个子类可以有多个父类

11.构造函数的多种继承方式及其优缺点

先定义一个Animal类:

function Animal(name){
	// 属性
	this.name = name || "Animal"
	
	//实例方法
	this.sleep = function(){
		console.log(this.name + "在睡觉")
	}
}

// 原型方法
Animal.prototype.eat = function(food){
	console.log(this.name + "在吃" + food)
}

① 原型链继承

方法:子类的原型指向父类的一个实例。这就是基于原型链的继承。

优点:

  1. 基于原型链,既是父类的实例,也是子类的实例。
  2. 可以继承父类的属性和构造方法,也能继承父类原型属性和方法。

缺点:无法实现多继承。

Animal.prototype.eat = function(food){
	console.log(this.name + "在吃" + food)
}

function Cat(){
	this.age = 10
}
Cat.prototype = new Animal()
Cat.prototype.name = "tom"
//以上两行也可以改为Cat.prototype = new Animal("tom")

// 测试
var cat = new Cat()
console.log(cat.name)	//tom
console.log(cat.age) //10
cat.eat("fish") //tom在吃fish
cat.sleep() //tom在睡觉
console.log(cat instanceof Animal) //true
console.log(cat instanceof Cat) //true
console.log(cat.constructor == Animal) //true 若不指定,则指向父类
console.log(cat.constructor == Cat) //false

// 指定构造函数
Cat.prototype.constructor = Cat
console.log(cat.constructor == Cat) //true

② 构造继承

方法:调用父类的构造函数来实现继承。相当于复制父类的构造属性和方法给子类。

优点:

  1. 可以实现多继承(继承多个父类的构造方法)
  2. 实例只是子类的实例,不是父类的实例
  3. 子类保留父类构造函数可传参的优势,如下例子传name

缺点:只能继承父类的构造属性和方法,无法继承父类的原型属性和方法。

function Dog(name){
	Animal.call(this,name) //调用父类构造方法,将this传与父类
        //这里可以调用多个父类,来实现多继承
}

// 测试
var dog = new Dog("wangwang")
console.log(dog.name)	//wangwang
dog.eat("rou")	//Uncaught TypeError: dog.eat is not a function
dog.sleep() //wangwang在睡觉
console.log(dog instanceof Animal) //false
console.log(dog instanceof Dog) //true
console.log(dog.constructor == Animal) //false
console.log(dog.constructor == Dog) //true

③ 组合继承

方法:即使用原型链继承和构造继承组合的方式实现继承。通过调用父类构造,继承父类的属性并保留传参的优点。然后通过将父类实例作为子类的原型,实现继承。

优点:

  1. 可以继承实例属性方法,也可以继承原型属性和方法。
  2. 可以实现多继承。

缺点:调用了两次父类函数,影响了性能,增加了内存负担。(在构造函数中调用了一次父类构造方法,在原型链继承时又初始化了一次父类)

function Cat(name){
	Animal.call(this,name);
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 测试
var cat = new Cat("tom")
console.log(cat.name)	//tom
cat.sleep()	//tom在睡觉
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
console.log(pig.constructor == Animal) //false 由于重新将Cat赋值给了constructor
console.log(pig.constructor == Cat) //true

④ 寄生类继承

方法:通过寄生类(中间类)方式,实现原型继承。将父类的原型直接赋值给中间类,然后将中间类的实例赋给子类原型。

优点:由于原型链prototype是引用类型,无法直接将其赋值给子类的原型,会被子类直接修改。所以赋值给中间类,然后将中间类的实例赋给子类原型,避免了原型引用造成的原型开放(易被子类修改)。且直接赋值原型给寄生类,避免初始化父类而再次执行父类造成内存浪费。

缺点:只能继承父类的原型属性和方法,无法继承父类的构造属性和方法。

function Pig(name){
	this.name = name //这里无法继承父类属性,需要重新定义
}

//创建寄生类实例
function superObj(proto) {
    function Super() {}
    Super.prototype = proto
    return new Super()
}

//将寄生类实例作为子类的原型
Pig.prototype = superObj(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //pig.sleep is not a function
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

以上寄生类方法可以使用Object.create()方法取代。

function Pig(name){
	this.name = name //这里无法继承父类属性,需要重新定义
}

//将寄生类实例作为子类的原型
//Object.create()方法会创建一个新对象,类似于实例化的类
Pig.prototype = Object.create(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //pig.sleep is not a function
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

⑤ 寄生组合继承-推荐

方法:通过寄生类(中间类)方式,实现原型继承。将父类的原型直接赋值给中间类,然后将中间类的实例赋给子类原型。

优点:

  1. 避免初始化父类两次造成内存浪费。
  2. 既能继承父类的原型属性和方法,也能继承父类的构造属性和方法。
  3. 可实现多继承。
function Pig(name){
	Animal.call(this,name); //调用父类,将其属性和方法拷贝给子类
}

//将寄生类实例作为子类的原型
Pig.prototype = Object.create(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //佩奇在睡觉
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

可在构造函数中调用多个父类,实现构造属性和方法的多继承。原型属性和方法多继承可以使用Object.assign(cur.prototype,otherFather.prototype)方法实现。

function Pig(name){
	Animal.call(this,name);
	Object1.call(this)
	Object2.call(this)
}

//将寄生类实例作为子类的原型
Pig.prototype = Object.create(Animal.prototype)

//Object.assign 会把其他原型上的属性和方法拷贝到Pig原型上
//只是原型方法的拷贝,而实例原型还是Animal
//若不支持Object.assign,则使用for.in遍历将对象合并
Object.assign(Pig.prototype,
	Object1.prototype,
	Object2.prototype,
	{a:1,b:2},
)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //佩奇在睡觉
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig instanceof Object1) //false
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false
console.log(pig.constructor == Object1) //false

12.this指向问题

  1. 默认绑定:全局环境中,this默认绑定到window。独立函数调用(直接调用,不使用a.b())都指向window。
  2. 隐式绑定:在对象字面量中,this隐式绑定到该对象。如:var obj = {a:1, fn:function(){ console.log(this.a) } }
  3. 隐式丢失:被隐式绑定的函数丢失绑定对象,从而this被绑到window。
  4. 显式绑定:使用call/apply/bind方法把对象绑定到this上
  5. new绑定:用new关键字实例化构造函数,this被绑到到实例化对象上。
  6. 函数中的this指向当前调用函数所处的环境,谁调用了它,this就指向谁。
  7. this指向和书写位置无关,和调用方式有关(this是在函数调用时入栈的执行上下文中动态绑定的)
  8. 优先级:new > 显式绑定(bind) > 隐式绑定 > 默认绑定

独立函数调用:

fn1(){
  console.log(this)  // window
}
fn2(){
  console.log(this)  // window
  fn1()
}
fn2()

// 以上都是window
var a = 1
function foo() {
	console.log(this.a)
}
foo()  //1
//函数中的this指向取决于谁调用了它
//foo() 相当于 window.foo(),所以指向window
//而var声明的变量会被挂载到window上

var obj = {
	a: 2,
	foo: foo
}
obj.foo() //2
//这里是obj调用了foo()所以this指向obj

// 以上两者情况 this只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,this`只会绑定在 c上,不会被任何方式修改 this指向
var c = new foo()
c.a = 3
console.log(c.a)  //3

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

构造函数默认不使用return关键字,他们通常初始化新对象。当构造函数函数体执行完毕后会显式返回这个对象。

箭头函数this指向于定义时所处的外层非箭头函数(注意这里是函数,若为对象则无效,再找外层)环境(取决于他外面的第一个不是箭头函数的函数this),一旦声明(绑定了上下文)无法改变。

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
a()()()  //window
//等同于window.a()()()
//这里的this指向函数a的this,而函数a的this在调用时是window调用的,所以指向window

//--------------------------------

var obj = {
	fn:() => {
		console.log(this)
	},
	fn2:function() {
		console.log(this)
	}
}
obj.fn() //window 由于箭头函数定义时外层是对象不是函数,那么再找外层,所以是window
obj.fn2() //obj

13.instanceof原理

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。若找不到,会一直判断父类的prototype,直至Object

手写一个 instanceof

function _instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__  //这里如果找不到则找其原型的原型。比如数组最终原型指向Object
    }
}

_instanceof([],Array) //true
_instanceof([],Object) //true
[] instanceof Array //true
[] instanceof Object //true

前端重点知识总结—HTML/HTML5

2018
12/05

1. HTML5新增了哪些新特性?

新元素:

  1. 用于媒介的 video 和 audio 元素
  2. 语意化更好的内容元素,比如 article、footer、header、nav、section
  3. 新的表单控件,calendar、date、time、email、url、search、color
  4. 本地存储 localStorage和sessionStorage
  5. JS绘图 canvas

2.WebSocket的实现和应用

① 什么是WebSocket

WebSocket是html5中的协议,支持持久连接。http协议不支持持久连接。

② WebSocket特点

  1. 基于Http协议,或者说借用了http协议来完成一部分握手。在握手阶段与http是相同的。
  2. 可以取代 Ajax 轮询。
  3. 一个WebSocket握手协议的实现,基本有两个属性,告诉服务器发送的是WebSocket:upgrade:websocketconnection:Upgrade

② WebSocket使用

// 打开一个 web socket
var ws = new WebSocket("ws://localhost:9998/echo");

ws.onopen = function(){
  // Web Socket 已连接上,使用 send() 方法发送数据
  ws.send("发送数据");
  alert("数据发送中...");
};

ws.onmessage = function (evt) { 
  var received_msg = evt.data;
  alert("数据已接收...");
};

ws.onclose = function(){ 
  // 关闭 websocket
  alert("连接已关闭..."); 
};

3.HTML5拖放事件使用

拖放(Drag 和 drop)是 HTML5 标准的组成部分。

  1. ondragstart:事件主体是被拖放元素,在开始拖时触发
  2. ondarg:事件主体是被拖放元素,正在拖放时触发。
  3. ondragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
  4. ondragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
  5. ondragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。
  6. ondrop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
  7. ondragend:事件主体是被拖放元素,在整个拖放操作结束时触发

例子

<!--拖动图片到矩形框中:-->
<style>
#div1{width:500px;height:300px;border:2px solid red;}
</style>
<div id="div1" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<!--draggable="true" 设置元素可被拖放-->
<img id="drag1" width="500" height="300" src="https://code.zuifengyun.com/wp-content/uploads/2021/06/d213d14979c83b01635bb8994fbe46b9f9e6ce84-700x280.jpeg" draggable="true" ondragstart="drag(event)">
<script>
    function allowDrop(ev){
	    ev.preventDefault();
    }
    function drag(ev){
	    ev.dataTransfer.setData("Text",ev.target.id);
    }
    function drop(ev){
	    ev.preventDefault();
	    var data=ev.dataTransfer.getData("Text");
	    ev.target.appendChild(document.getElementById(data));
    }
</script>

4.对HTML语义化标签的理解

HTML5语义化标签是指正确的标签包含了正确的内容,结构良好,便于阅读,比如nav表示导航条,类似的还有article、header、footer等等标签。

5.用一个div模拟textarea的实现

内容可编辑:给div添加contenteditable=true即可。

<style>
div{
    width: 400px;
    min-height: 100px;
    max-height: 300px;
     _height: 100px; //IE6
    margin-left: auto;
    margin-right: auto;
    padding: 3px;
    outline: 0;
    border: 1px solid #a0b3d6;
    font-size: 12px;
    word-wrap: break-word;
    overflow-x: hidden;
    overflow-y: auto; //超过最大高度就出现滚动条
    _overflow-y: visible;
}
</style>
<div contenteditable="true">
    .....此处省略.....
</div>

6.input与textarea的区别

<input>是单行文本框,不会换行。通过size属性指定显示字符的长度,注意:当使用css限定了宽高,那么size属性就不再起作用。value属性指定初始值,maxlength属性指定文本框可以输入的最长长度。可以通过width和height设置宽高,但是也不会增加行数。

<textarea>  是多行文本输入框,文本区中可容纳无限数量的文本,无value属性,不可通过value进行预设值,但可以使用value获取其值。其中的文本的默认字体是等宽字体(通常是 Courier) ,可以通过 cols 和 rows 属性来规定 textarea 的尺寸,不过更好的办法是使用 CSS 的 height 和 width 属性。

7.iframe有哪些缺点?

  1. iframe会阻塞主页面的onload事件
  2. iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
  3. iframe框架结构不好,如果嵌套多个iframe页面的货出现多个滚动条,用户体验差;
  4. iframe不利于搜索引擎优化;
  5. 很多移动设备无法完全显示框架,设备兼容性差;
  6. iframe框架页面会增加很多的http请求,增加服务器负担;
  7. 如果要使用最好是通过js动态给iframe添加src属性值,这样避免页面阻塞。

8.移动端适配之viewport

什么是viewport

<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />

手机浏览器是把页面放在一个虚拟的窗口-viewport中的,通常情况下这个虚拟的窗口比屏幕宽,这样就不用吧每个网页挤到很小的窗口中(这样会破坏没有针对手机浏览器优化的网页的布局),此时虽然显示不完全,但是用户仍可以通过平移和缩放来看网页的内容。viewport就是让网页开发者通过其大小,动态的设置其网页内容中控件元素的大小,从而使得在浏览器上实现和web网页中相同的效果(比例缩小)。

  1. width:控制viewport的大小,一般情况下指定为device-width(终端宽度,单位为缩放为100%的CSS像素),也可以指定一个固定的值例如600.
  2. height:和width相应,指定高度。
  3. initial-scal:初始缩放比例,页面第一次load的时候的缩放比例。
  4. maximum-scale:允许用户缩放到的最大比例。
  5. minimum-scale:允许用户缩放到的最小比例。
  6. user-scalable:用户是否可以手动缩放。

前端重点知识总结—HTTP

2018
06/05

1.http和https的基本概念

http: 超文本传输协议,是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。

https: 是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。https协议的主要作用是:建立一个信息安全通道,来确保数据的传输,确保网站的真实性。(简单来说https协议是由http和ssl协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。)https的SSL加密是在传输层实现的。

2.http和https的区别?

  1. Https协议需要ca证书,费用较高。
  2. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  3. 使用不同的链接方式,端口也不同,一般而言,http协议的端口为80,https的端口为443
  4. http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

3.https协议的工作原理

客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤:

  1. 客户使用https url访问服务器,则要求web 服务器建立ssl链接。
  2. web服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),返回或者说传输给客户端。
  3. 客户端和web服务器端开始协商SSL链接的安全等级,也就是加密等级。
  4. 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给服务端。
  5. web服务器通过自己的私钥解密出会话密钥。
  6. web服务器通过会话密钥加密与客户端之间的通信。

4.https协议的优点

  1. 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
  2. HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
  3. HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
  4. 谷歌搜索算法,采用HTTPS加密的网站在搜索结果中的排名将会更高。

5.https协议的缺点

  1. https握手阶段比较费时,会使页面加载时间延长50%,增加10%~20%的耗电。
  2. https缓存不如http高效,会增加数据开销。
  3. SSL证书也需要购买,功能越强大的证书费用越高。比如使用泛域名SSL证书。
  4. SSL证书需要绑定IP,不能再同一个ip上绑定多个域名,ipv4资源支持不了这种消耗。

6.tcp三次握手,一句话概括

客户端和服务端都需要知道各自是否可以进行收发,因此需要三次握手。相当于敲门,用于确认是否可以开始发送。

三次握手可以简化为:C发起请求连接,S确认,S也发起连接,C确认。我们再看看每次握手的作用:第一次握手:S只可以确认自己可以接受C发送的报文段;第二次握手:C可以确认S收到了自己发送的报文段;并且可以确认自己可以接受S发送的报文段;第三次握手:S可以确认C收到了自己发送的报文段。

7.说一下http2.0

简要概括:http2.0是基于1999年发布的http1.0之后的首次更新。

提升访问速度:请求资源所需时间更少,访问速度更快,相比http1.0。

多路复用:多路复用允许同时通过一个TCP连接发送多个请求。改善了:在http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制(连接数量),超过限制会被阻塞。

头部压缩:相同请求不需要再次发送请求头等信息,通信期间几乎不会改变的请求头的通用的键值,如user-Agent和content-Type只发送一次,相当于做了一层缓存

二进制分帧:HTTP2.0会将所有的传输信息分割为更小的信息或者帧,并对他们进行二进制编码。

8.400和401、403状态码

(1)400状态码:请求无效

产生原因:

前端提交数据的字段名称和字段类型与后台的实体没有保持一致

前端提交到后台的数据应该是json字符串类型,但是前端没有将对象JSON.stringify转化成字符串。

解决方法:

对照字段的名称,保持一致性

将obj对象通过JSON.stringify实现序列化

(2)401状态码:当前请求需要用户验证

(3)403状态码:服务器已经得到请求,但是拒绝执行

9.HTTP支持哪些请求方式

get、post、head、options、put、delete、trace、connect

10.HTTP请求的options,head方式

head:类似于get请求,只不过返回的响应中没有具体的内容,用户获取报头

options:允许客户端查看服务器的性能,比如说服务器支持的请求方式等等。

11.GET和POST的区别

  1. 副作用指对服务器上的资源做改变,比如搜索是无副作用的,注册是副作用的。Get 多用于无副作用场景,Post 多用于副作用场景。
  2. get参数通过url传递,post放在request body(请求体)中。
  3. get请求连接长度是有限制的(浏览器或服务器限制,HTTP不限制),post没有。
  4. post比get更加安全。因为get参数直接暴露在url中,不能用来传递敏感信息。
  5. get只能进行url编码。而post可以通过 request body来传输比 Get 更多的数据,支持多种编码方式且不对数据类型限制,如application/x-www-form-urlencodedmultipart/form-dataapplication/jsontext/xml
  6. get参数会被浏览器主动缓存,保留在浏览器历史记录中,而post参数不会
  7. get和post本质都是tcp连接,但由于浏览器和服务器的限制导致传输有一些差异。
  8. get产生一个tcp数据包,post产生两个tcp数据包。get方式会把请求头和data数据一并发出,服务器响应200;post请求先发请求头,服务器响应100,再发请求体数据,服务器响应200。

12.浏览器缓存机制

详见:浏览器的缓存机制详解

13.前端http优化

  1. 减少http请求数(http/1):合并css,合并JS,合并图片。但这种优化方式在http2.0中无效。
  2. 减少DNS查询时间
  3. 使用CDN
  4. 利用浏览器缓存:传输轻量、细粒度的资源,以便独立缓存和并行传输。因为HTTP/2的多路复用和头部压缩特性。
  5. 最小化HTTP请求大小
  6. 最小化HTTP响应大小
  7. 减少不必要的重定向
  8. 服务端渲染:允许服务端主动发送额外的资源。类似于ssr首屏的处理。

http2.x多路复用

多路复用使得不同的请求共用一个TCP连接,允许多个资源并行下载,避免建立多个连接带来不必要的额外开销。它消除了HTTP/1.1中的线头阻塞问题。头部压缩进一步减少了多个HTTP请求的开销,因为每个请求开销都小于未压缩的等价HTTP/1.1请求。

PS:文件合并依然可以提高压缩率,但它带来了代价高昂的缓存失效。即使有一行CSS改变了,浏览器也会强制重新加载你 所有的 CSS声明。另外,不是所有页面都使用了合并后的CSS或JavaScript文件中的全部声明或函数。

14.列举HTTP状态码,及如何获取

在原生xhr请求完成可通过XMLHttpRequest的实例中的status属性来获取。

var xhr = new XMLHttpRequest()
// open方法的第三个参数是是否异步
xhr.open('GET', 'http://xxxx.com/api?a=1&b=2' ,true)
xhr.send(null)
xhr.onreadystatechange = () =>{
	if(xhr.readyState==4){
		console.log(xhr.status, xhr.responseText)
	}
}
  • 100,接受的请求正在处理,信息类状态码
  • 2xx(成功)表示成功处理了请求的状态码
  • 200(成功)服务器已成功处理了请求
  • 206,服务器成功处理了部分请求,比如做分段下载(head方法获取文件大小,请求头字段用Range)
  • 3xx(重定向)表示要完成请求,需要进一步操作。通常这些状态代码用来重定向。
  • 301,永久性重定向,表示资源已被分配了新的 URL
  • 302,临时性重定向,表示资源临时被分配了新的 URL
  • 303,表示资源存在另一个URL,用GET方法获取资源
  • 304,(未修改),http请求的缓存问题引起,服务端不会返回新内容
  • 307,临时重定向
  • 4xx,(请求错误)这些状态码表示请求可能出错,妨碍了服务器的处理
  • 400,(错误请求)服务器不理解请求的语法
  • 401,当前请求需要用户验证
  • 403,(禁止)服务器拒绝请求(服务器已经得到请求,但是拒绝执行)
  • 404,(未找到)服务器找不到请求网页
  • 5xx,(服务器错误)这些状态码表示服务器在尝试处理请求时发生内部错误。
  • 500,(服务器内部错误)服务器遇到错误,无法完成请求
  • 502,服务端负载比较高,导致连接超时
  • 503,表示服务器宕机,无法处理请求

15.TCP和UDP的区别(了解)

UDP:

  • 面向无连接,不需要三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
  • 支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
  • 面向报文
  • 不可靠性:通信都不需要建立连接,想发就发,也不会关心对方是否已经正确接收到数据了,这样的情况肯定不可靠。
  • 恒定速率:UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
  • 头部开销小,传输数据报文时是很高效的。
  • 没有缓存,不会备份数据。

TCP:

  • 面向连接的、可靠的、基于字节流的传输层通信协议。流就是指不间断的数据结构。
  • 三次握手:相当于敲门,告知和确认可以开始收发。防止出现失效的连接请求,从而产生错误。这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
  • 只能进行点对点的数据传输,不支持多播和广播传输方式。
  • 每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收
  • 拥塞控制:当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞
  • 全双工通信:TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段
  • TCP 针对数据包丢失的情况,可以采用重传机制解决

16.Chrome浏览器HTTP最大请求并发数限制

同一域名下,同一GET请求的并发数是1,也就是说上一个请求结束,才会执行下一个请求,否则置入队列等待发送;

同一域名下,不同GET/POST请求的并发数量是6。当发送的请求数量达到6个,并且都没有得到响应时,后面的请求会置入队列等待发送。

CSS重点知识总结—响应式布局

2017
17/11

1.rem布局的优缺点

rem本质是等比缩放。子元素尺寸设置rem单位,通过更改html元素的字体大小,就可以让子元素实际大小发生变化。

缺点:

  1. pc页面来讲使用次数不多,一般移动端可能会用;全适配方案可以使用媒体查询@media
  2. 数据量大:所有的图片,盒子都需要我们去给一个准确的值,才能保证不同机型的适配

2.常用的响应式布局有哪些方法

首先,在网页代码的头部,需要加入加入一行viewport元标签。作用为:网页宽度默认等于屏幕宽度(width=device-width),原始缩放比例(initial-scale=1)为1.0,即网页初始大小占屏幕面积的100%。

<meta name="viewport" content="width=device-width, initial-scale=1" />

css样式响应式的一些方法:

  1. 使用一些响应式单位如:em,rem,vw,vh,vmin,vmax,ch (小程序rpx)
  2. 尺寸使用百分比。
  3. 使用@media媒体查询。
  4. 使用max-widthmax-width等限制元素尺寸。
  5. 使用flex弹性布局。

3.常用的响应式单位有哪些

  • % 父元素尺寸的百分比为当前元素尺寸
  • em 优先根据自身的字体大小,如果没有就向上寻找最近父元素有设置字体大小来换算的倍数尺寸
  • rem 根元素(html元素或:root元素)设置的字体大小的倍数尺寸
  • ch 相对于数字0的大小。 1ch 就是数字 0 的宽度
  • vw 相对于视口(浏览器可视区域)的宽度,视口宽度会被均分为100单位,则1vw等于视口宽度的1%
  • vh 相对于视口的高度,视口高度会被均分为100单位,则1vh等于视口高度的1%
  • vmin 选取vw和vh中最小的那个均分为100单位来换算
  • vmax 选取vw和vh中最大的那个均分为100单位来换算

4.媒体查询的使用方式有哪些

① 直接在css中编写

@media 类型 and (条件1) and (条件二){
    css样式
}
//例:
@media screen and (max-width:980px ) {
    body{
        background-color: red;
    }
}

② 使用@import导入

@import url("css/moxie.css") all and (max-width:980px);

③ 也是最常用的:使用link连接,media属性用于设置查询方式:

<link rel="stylesheet" media="mediatype and|not|only (media feature)" type="text/css" href="mystylesheet.css">

<!-- 例子 -->
<link rel="stylesheet" media="screen and (min-width: 600px) and (max-width: 800px)" type="text/css" href="style.css">

前端基础知识总结—理论篇

2017
12/10

1.重绘与重排

重排(也叫回流)reflow/Relayout

当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。

浏览器渲染页面:浏览器渲染是基于“流式布局”的模型,流实际就使我们常说的文档流。浏览器根据渲染树中每个渲染对象的信息,计算出各自渲染对象的几何信息(DOM对象的位置和尺寸大小),并将其安置在界面中的正确位置。

会引起重排的操作有

  1. 页面首次渲染。
  2. 浏览器窗口大小发生改变。
  3. 元素尺寸或位置发生改变。
  4. 元素内容变化(文字数量或图片大小等等)。
  5. 元素字体大小变化。
  6. 添加或者删除可见的DOM元素。
  7. 激活CSS伪类(例如::hover)。
  8. 设置style属性
  9. 查询某些属性或调用某些方法。

重绘repaint

当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此损耗较少。比如修改字体颜色、背景、边框类型、阴影、显隐等。

重排一定会重绘,重绘不一定有重排。

避免重排

  1. 避免设置大量的style属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性。
  2. 实现元素的动画,它的position属性,最好是设为absoulte或fixed,这样不会影响其他元素的布局。
  3. 动画实现的速度的选择。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。
  4. 不要使用table布局,因为table中某个元素旦触发了reflow,那么整个table的元素都会触发reflow。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
  5. 如果在一个局部方法中需要多次访问同一个dom,则先暂存它的引用,避免多次访问dom。
  6. 少用DOM集合(类数组)来遍历,因为集合遍历比数组遍历耗费更高。
  7. 用事件委托来减少事件处理器的数量。

2.HTML和DOM的区别

  1. html是一种文档类型、是一种结构化文档、是一种标记语言。
  2. dom是具有一些API的文档对象模型(对象)。提供了一系列的对页面文档访问和操作的api。
  3. dom还可以操作其他的结构化文档,比如XML DOM 定义了一套标准的针对 XML 文档的对象。

3. 网页从输入网址到渲染完成经历了哪些过程?

大致可以分为:

  1. 输入网址;
  2. 查询本地DNS缓存,如果有这条记录,则直接返回对应的IP。否则请求DNS服务器,并获取对应的服务器ip地址(返回给客户机,并缓存这条记录),如果做了CDN加速的话,会提供访问速度最快的 IP 地址;
  3. 浏览器通过IP与端口号与服务器建立TCP连接,请求数据。(会与服务器进行3次握手)
  4. 浏览器向web服务器发送http请求,请求方式如get/post/delete/put/option等。数据在进入服务端之前,可能还会先经过负责负载均衡的服务器或网关,它的作用就是将请求合理的分发到多台服务器或端口上或进行重定向。
  5. web服务器响应请求,并返回指定url的HTTP报文(或错误信息);
  6. 首先浏览器会判断返回的状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错。
  7. 一次会话完成,关闭
  8. 浏览器解析源文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件。
  9. 解码完成后浏览器开始渲染流程,生成DOM树,解析css和js,渲染页面,直至呈现完成;(具体的渲染过程参考下文)

一文吃透浏览器渲染基本原理

4.什么是同源策略?

两个页面地址中的协议、域名和端口号一致,则表示同源。

同源策略的限制:

  1. 存储在浏览器中的数据,如localStroage、Cooke不能通过脚本跨域访问
  2. 不能通过脚本操作不同域下的DOM
  3. 不能通过ajax请求不同域的数据
  4. 跨域请求不能携带cookie信息,需要配置响应头”Access-Control-Allow-Credentials”: true

为什么会有同源策略:

目的是为了安全,如果没有同源限制,在浏览器中的cookie等其他数据可以任意读取,不同域下的DOM任意操作,ajax任意请求其他网站的数据,包括隐私数据。

5. 什么是跨域,解决跨域所有方法

用ajax访问非同源的接口地址,或非同源的页面直接传递数据,就会存在跨域问题。

解决跨域的办法:

1.通过jsonp跨域

缺点:

只能使用Get请求

不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败

JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保

2.通过修改document.domain来进行跨域

缺点:只能跨子域

3.HTML5新引进的window.postMessage方法实现跨域传参

缺点:会阻断后续程序的执行。所以只能放到程序最后。

4.CORS跨域(常用),服务器返回数据时修改响应头,使浏览器认为是同源。

Access-Control-Allow-Origin: *  //该字段必须,表示接受任意域名的请求

Access-Control-Allow-Methods: POST, GET, OPTIONS    //必须,永续请求的方式

Access-Control-Allow-Headers: X-PINGOTHER, Content-Type  //可选,允许接受除了6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma外的其他字段。

Access-Control-Max-Age: 86400  //指定本次预检请求的有效期,单位为秒。一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin请求头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

Access-Control-Allow-Credentials: true  //允许浏览器发送cookie

详细信息如下文:

Access-Control-Allow-前后端设置跨域CORS详解

5.动态创建script,script标签不受同源策略的限制。具体没考究。

6.iframe中使用利用location.hash 跨域传值

7.web socket协议进行跨域,建立长连接可以跨域。服务器需要配置支持web socket协议。

8.使用网关做反向代理,借助中间层。如nginx反向代理,node中间层反向代理。

6.图片懒加载和预加载

预加载:提前加载图片,当用户需要时直接从缓存中渲染。预加载是为了避免图片的加载过慢造成较差的用户体验,但是会增加服务器前端的压力。

懒加载:当浏览器滚动到相应的位置时再渲染相应的图片。目的是优化前端压力,减少请求数或延迟请求。对服务器前端有一定缓解压力作用。但是若图片较大,则会带来较差的用户体验。

懒加载实现方法:

图片懒加载实现方式

7.简单解释cookie和session

  1. cookie数据存放在本地浏览器,session数据存放在服务器。
  2. cookie并不是很安全,别人可以分析和修改cookie,考虑到安全赢个结合session使用。
  3. cookie结合session可以用来进行登陆或请求验证。session会保存到服务器上,有失效时间。cookie中保存session id
  4. 单个cookie数据不能超过4k,且很多浏览器限制了一个站点cookie的数量。比如20个。
  5. 由于安全性,现代ajax请求通常采用token方式取代cookie-session

8.异步加载js方式

① async属性

<script type="text/javascript" src="xxx.js" async="async"></script>
  1. async属性是HTML5新增属性,需要Chrome、FireFox、IE9+浏览器支持
  2. async属性规定一旦脚本可用,则会异步执行
  3. async属性仅适用于外部脚本
  4. 此方法不能保证脚本按顺序执行
  5. 他们将在onload事件之前完成

② defer属性

<script type="text/javascript" defer></script>
  1. defer属性规定是否对脚本执行进行延迟,直到页面加载为止
  2. 如果脚本不会改变文档的内容,可将defer属性加入到<script>标签中,以便加快处理文档的速度
  3. 兼容所有浏览器
  4. 此方法可以确保所有设置了defer属性的脚本按顺序执行
  5. 在IE中同时存在defer和async属性时,defer的优先级比较高

③ 异步创建script标签,插入到DOM

(function(){
    var scriptEle = document.createElement("script");
    scriptEle.type = "text/javasctipt";
    scriptEle.async = true;
    scriptEle.src = "http://cdn.bootcss.com/jquery/3.0.0-beta1/jquery.min.js";
    var x = document.getElementsByTagName("head")[0];
    x.insertBefore(scriptEle, x.firstChild);		
 })();

9. CDN是什么

CDN的全称是Content Delivery Network,即内容分发网络。其通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵跨运营商跨地域跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。 因为CDN的这些特性,我们可以将体积较大的文件或是图片上传到CDN中,通过CDN来加载,减轻了服务器的请求压力,同时也可以通过CDN来获取、加载依赖。