标签: javascript

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

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 } )

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标签是两个代码块。报错不会影响另一处。但如果后面的代码块有引用前面的变量或对象,是会出错。

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(实现双向绑定、语法糖)

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