你需要知道的JavaScript知识|牛气冲天新年征文
这篇文章将会说一些javascript的基础知识, 这篇文章非常适合你复习javascript基础以及对一些新手有所帮助, 请认真查看文章的中的代码, 对你理解有所帮助
call、apply、bind之间的区别和简单实现
共同点
其实都是为了改变我的this指向
不同点
1. 函数是否会调用
- call和apply都可以调用我的函数
- 而我的bind它不会调用我的函数,它会返回一个新的函数,您必须手动的调用它
2. 传参的不同
- call、bind如果函数需要传参,那么您需要依次传入对应的参数
- apply函数传参,需要封装成数组传递
call实现
在实现之前,我们先来看下MDN中对这个方法的定义吧
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
也就是我们需要实现有以下几点:
- 我必须是一个函数才能调用该方法,因为call是在Function.prototype中的方法
- 不传参数或者第一个参数传 null,this 指向 window
- 第一个参数之后的参数作为调用函数的传参接收
- 改变函数 this 指向,返回调用函数执行结果
// 调用该方法必须是一个函数,可以看到是在Function.prototype中添加的方法
Function.prototype.myCall = function (context) {
// 判断我的对象是null或者没有传入对应值,如果是的话,就是指向window
context = context || window
// 给我的对象添加一个fn属性,fn的值就是调用该方法的函数
context.fn = this
// 将我的形参进行通过三点运算符进行解构,并且截取第一个参数后的参数
const args = [...arguments].slice(1)
// 调用我的方法, 并且把对应的参数数组进行解构作为实参调用
const result = context.fn(...args)
// 函数调用完毕,这个函数是放在我的对象上的,调用完毕,可以删除了
delete context.fn
// 返回执行结果
return result
}
var name = 'yaojin'
const obj = {
name: '遥近'
}
function showName (age) {
console.log(age)
// 参数可以接受到输入了18
console.log(this.name)
}
//普通调用该函数
showName(18) // yaojin
//通过myCall
showName.myCall(obj, 18)// 遥近
}
apply的实现
apply的实现其实跟call一致,只是apply传参的话需要是一个数组
var name = 'yaojin'
function showName (age, money) {
console.log(age)
console.log(money)
// 参数可以接受到输入了18
console.log(this.name)
}
const obj = {
name: '遥近'
}
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') throw new TypeError('Error');
context = context || window
context.fn = this
let result
// 判断是否有传参, 如果有传参,读取第二个参数,它是一个数组不然就会报错
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
// 如果没有传参就正常调用该函数
result = context.fn()
}
delete context.fn
return result
}
showName() //this指向的是windwo 读取了 yaojin
showName.myApply(obj, 18) // 遥近 this指向了obj
bind的实现
bind方法跟call和apply最大的区别就是它会返回一个函数,所以要实现返回一个函数,并且改变其this指向,而this指向分2种情况,一种是指定的,一种是,如果通过new调用的话this指向其新创建的对象
Function.prototype.myBind = function(context) {
let _this = this // 缓存调用该函数的函数
let args = [...arguments].slice(1) // 去掉第一个传入的参数
var fn = function () {
let bindArgs = [...arguments] //将所有参数保存到一个数组中
// 如果通过new调用,那么this指向我的函数,如果不是的话,this指向我传入的对象,并且传入对应的参数
return _this.apply(this instanceof fn ? this : context, args.concat(bindArgs))
}
// 如果是通过new调用,需要将我新创建的对象的this的prototype指向我函数的prototype
fn.prototype = Object.create(this.prototype)
return fn
}
const name = 'yaojin'
const obj = {
name: '遥近'
}
function showName (age, address) {
this.sex = '男'
console.log(this.name)
console.log(age)
console.log(address)
}
showName.myBind(obj, 18)("佛山") // this指向obj, 打印遥近,并且相应的参数也可以打印
const newFn = showName.myBind(obj, 18)
var newObj = new newFn('佛山') // 通过new调用
console.log(newObj.sex) // 打印男,我的新对象能继承我函数里面的属性和方法了
New关键字实现了啥?
- 创建一个空的对象,这个对象就是我最终要显示的结果(设我新的对象是obj)
- 将我新创建的对象的隐式原型指向构造函数的显示原型( obj.proto === xxx.protptype )
- 通过apply来改变我构造函数的this指向,并且将我对应的形参传入到我的构造函数中,对其进行调用,最终就会返回对应的结果了
- 最后一点需要注意都是,返回值的问题,如果在构造函数中有返回其他对象,那么最后会返回这个对象,如果没有的话,会返回我新创建的对象,并且我构造函数中的this会指向我新创建的对象
以下是new 实现的相应代码模仿
<script type="text/javascript">
function _new() {
// 创建一个空的对象
let target = {};
// 把我传入的实参进行展开,赋值到一个新的数组当中,这个数组的第一个就是我的构造函数,其他就是构造函数中对应的实参
let [constructor, ...args] = [...arguments];
// 新创建的对象的隐式原型指向构造函数的显示原型,让我新创建的对象可以继承构造函数的方法和属性
target.__proto__ = constructor.prototype;
//调用构造函数,并且把我构造函数的this指向我的新创建的对象,传入实参,将函数的返回值,赋值到result;
let result = constructor.apply(target, args);
// 判断我的result是否是一个对象,如果是的话就返回这个对象,不是的话我就返回我新创建的对象;
if (result && (typeof (result) === "object" || typeof (result) === "function")) {
return result;
}
return target;
};
// 自定义的构造函数,接受2个形参
function Person(name, age) {
this.name = name;
this.age = age;
};
// 调用我创建的new方法,需要传入构造函数的名字,以及对应的2个形参
const yaojin = _new(Person,"遥近",18);
// 最后打印输出
console.log(yaojin.__proto__ === Person.prototype);
console.log(yaojin);
</script>
什么是闭包?
闭包很多人都说闭包是一个函数,但是我的理解它是一个对象,其实您可以通过debugger在控制台中可以看到它是一个对象,它包含了被引用的局部变量
控制台中一个名为Closure的对象就是闭包,您可以看到它有2个属性,就是我相应的变量名(key)以及变量的值(value)
那么闭包是如何产生的呢?
闭包的产生需要满足3个条件
第一个条件函数必须嵌套,也就是要产生闭包,必须最少需要有2个函数(可以看到上图中,的确是有2个函数)
第二个条件就是引用了我外部函数的局部变量(可以看到上图中,inner函数引用了外部函数的相应变量)
第三个条件就是必须调用我外部的函数
闭包的作用是什么呢?
闭包的作用主要是通过作用域链让我内部的函数可以访问到外部函数的局部变量,或者让我的全局坏境能引用到内部函数的变量(具体您可以举例给面试官听),都是为了延长我变量的生命周期,因为没有闭包的存在,我的函数执行完毕,局部变量就会给释放!
闭包的弊端
东西都是有利弊的,虽然闭包能延长变量的生命周期,但是我函数执行完后, 函数内的局部变量没有释放就会造成内存泄露,解决的办法是,将其设置null
function fn(){
let yaojin = '遥近';
function me(){
console.log(yaojin)
}
return me;
}
let test = fn(); //指向的对象会一直保存在堆内存中
test = null; // 若不再使用,则让其指向的对象为空
谈谈this 的指向
正常情况下,执行函数的方式,就决定了this的指向
- 直接调用
var name = '遥近'
function fn (name) {
console.log(this)
console.log(this.name)
}
fn() // this指向window 输出遥近
- 对象调用
let obj = {
name: 'yaojin',
fn
}
obj.fn() // this指向obj 输出yaojin
- new调用
function Person (name) {
this.name = name
}
yaojin = new Person('遥近') // 构造函数的this指向yaojin
- call、apply、bind调用
var name = '遥近'
let obj = {
name: 'yaojin',
fn
}
let obj1 = {
name: 'yaojin666'
}
function fn (name) {
console.log(this)
console.log(this.name)
}
fn.call(obj) // 指向obj
fn.apply(obj)// 指向obj
const fn2 = fn.bind(obj)
fn2()// 指向obj
特殊情况下,this的指向
- 箭头函数
function fn1 () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
var name = 'yaojin'
const obj = {
name: '遥近'
}
const newFn = fn1.call(obj) // 将fn1的this指向obj
newFn() // 返回的函数是一个箭头函数,它的this指向也是obj
箭头函数的this看外层的是否有函数, 如果有,外层函数的this就是内部箭头函数的this, 如果没有,则this是window。
- 回调函数 定时器回调: this指向window dom事件监听回调: this指向绑定事件的对应dom元素
什么是构造函数
在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。它们的函数名一般首字母为大写
而其实准确的来说JavaScript是没有构造函数的,只有函数的构造调用,叫构造函数只是为了迎合其他语言,才这么说的!
因为JavaScript只有通过new调用才会创建出构造函数,而其他语言,构造函数根本就不是通过new来创建的,它有自己独特的语法,而我们只有通过new才能创建出构造函数,所以我们称为函数的构造调用
作用和弊端
构造函数的作用在于,让我的实例对象可以继承我构造函数中的属性和方法,但是它的缺点同一个对象实例,没法共享属性和方法,也就是说当我都是通过new某个构造函数创建实例对象的时候,我想这些对象都继承某个相同的方法,如果都写在我的构造函数当中,那么其实它们这2个方法不是相同的,它会在堆中开辟新的空间,这样就会造成内存的浪费了!
//创建一个构造函数
function Person (name, age) {
this.name = name
this.age = age
this.hello = function () {
console.log('Hello,遥近')
}
}
const yaojin = new Person('遥近', 18)
const yj = new Person('遥近666', 18)
console.log(yaojin.hello === yj.hello)//false
//把共有的方法和属性存放到prototype中
Person.prototype.say = function () {
console.log('遥近您好')
}
console.log(yaojin.say === yj.say)//true
于是有了原型链的存在了,把我一个实例上共有的方法和属性都放在prototype对象中!这样就可以通过原型链来一步步找到对应的属性和方法了!
什么是原型链?
原型指的是函数的prototype
属性,以及对象的__proto__
属性,而需要注意的是,因为我函数也属于对象,所以函数存在我刚刚述说的2个属性
而函数prototype
属性的值是一个对象,他至少有2个属性: constructor
、 __proto__
,但是Object.prototype
除外!
constructor
属性的值指向的是其 构造函数 本身
//创建一个构造函数
function Person (name, age) {
this.name = name
this.age = age
}
//通过instanceof可以看出prototype是一个对象
console.log(Person.prototype instanceof Object)//true
//constructor属性指向的是其构造函数本身
console.log(Person.prototype.constructor === Person)//true
//Function构造函数的__proto__的值指向Function.prototype
console.log(Function.__proto__ === Function.prototype)//true
//构造函数的prototype对象它的__proto__的值都是指向Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype)//true
const yaojin = new Person('遥近', 18)
//可以看到通过new创建的对象其__proto__指向其构造函数的prototype
console.log(yaojin.__proto__ === Person.prototype)//true
//say方法,yaojin对象本身没有,会延着原型链,最终在其构造函数prototype中找到
Person.prototype.say = function () {
console.log('遥近您好')
}
而对象的__proto__
属性它的值分4种情况
1.构造函数的Function的__proto__的值指向的是Function.prototype
2.通过 new 创建的对象,它的值指向的是其构造函数的prototype
3.所有构造函数的prototype对象,它的值指向的是Object.prototype
4.Object.prototype的__proto__指向的是null
而就是这种__proto__
属性组成的链式结构,就形成了原型链了
它的作用是让对象查找属性有一套规则,它会先从自身寻找对应的属性,没有就会从它构造函数的prototype
属性查找,最终来到Object.prototype,还是没有的话就会返回undefined
原型链图
网上流传的原型链图可以给你一个更好的认识
谈谈作用域和作用域链
作用域您可以理解成它是存放代码的地方,它决定了变量和对象的可访问范围
作用域分为全局作用域
和 局部作用域
全局作用域
只要能在任何地方都可以访问到并且使用它,那么该对象或者变量就具有全局作用域的特征
只要具有全局作用域特征的变量和对象,其实就是在window对象当中添加对应的变量和方法
function name () {
var yaojin = '遥近'
console.log(name)
function showAge() {
console.log(age) // 全局作用域下哪里都可以访问
console.log(name)
}
showAge()
}
var age = 18
console.log(window.name) // 全局作用域下的函数
console.log(window.age)// 全局作用域下的变量
name()
从上面的代码中您可以看到,我在函数的内部再次打印外层的函数,也是可以访问到的,并且通过window读取对应的函数和变量也是可以访问到的,这就是全局作用域的特征
局部作用域
局部作用域跟全局恰恰相反了,它只会在特定的区域才可以访问到,而局部作用域又可以细分为函数作用域
和 块级作用域
(ES6提出的)
- 函数作用域
函数作用域,顾名思义就是只有在函数中有效,在函数定义的变量和函数只有在函数内部能访问,在全局中无法访问,除非通过 闭包 的形式
function name () {
var yaojin = '遥近'
console.log(yaojin) // 函数作用域下的变量全局没法访问
}
name()
console.log(yaojin) // 报错
console.log(address) // 报错
我们可以看到在局部作用域下的变量和函数不是在任何地方都可以访问到的,这就是全局跟局部最大的区别
- 块级作用域
ES6提出新的let和const命令,这样使我们可以创建出块级作用域,通过let和const定义的变量和对象,它会限制该变量或对象的作用域只在的当前的代码块中有效,通过花括号包裹着的就属于一个块,并且不会存在 变量的提升
function name () {
console.log(yaojin) // 报错
function test () {
console.log(yaojin1) // 报错了
}
test()
let yaojin1 = 'yaojin2070' // 变量不会存在提升
}
const yaojin = 'yaojin'// 变量不会提升
name()
:::tip 作用域的作用就是为了隔离变量,不同的作用域下只能访问特定的变量,避免变量的滥用 :::
作用域链
作用域链就是多个作用域组成的,是由内到外形成的链式结构,它的作用是查找变量的一个规则
var yaojin = '遥近'
function check () {
function check1 () {
console.log(yaojin) // 通过作用域链最终在全局中找到,打印遥近
}
check1()
}
check()
作用域的查找规则就是,先会从自身的作用域中寻找,如果没有找到,就会从上一层作用域查找,最终到了全局作用域中还是找不到,就会报错
变量和函数的提升
变量或者函数提升就是将其提升到它们自己 作用域的最顶端
变量的提升
变量的提升仅仅只是声明的提升,也就是它不会将内容也提升上去
function test () {
console.log(yaojin) // 打印undefined
var yaojin = 'yaojin'
}
test()
可以从代码中看到,我们在yaojin这个变量定义前进行打印,可是它并不会报错,这是因为存在变量的提升,可是变量的提升仅仅只是将声明提升了,也是说最后的代码是如下:
function test () {
var yaojin
console.log(yaojin) // 打印undefined
yaojin = 'yaojin'
}
test()
:::tip 从变量的提升我们就需要注意,我们以后写代码尽量将变量写在某个作用域的顶部,这样的方便自己查看,也方便维护 :::
函数提升
函数提升会将整体的提升,它会将整个函数提升到它自身作用域的顶端
test()
function test () {
console.log('函数提升了,哪怕我最顶端执行函数')
}
可以看到代码并不会报错,而是会正常的调用,这就是函数的提升
而需要注意的是:
- 首先明确先有声明再有赋值
console.log(yaojin)
yaojin = '遥近'
var yaojin
可以看到var yaojin
会提升到作用域顶部,并且需要注意同样的声明只能存在一次
- 先有变量的提升,再有函数的提升
console.log(yaojin)// 打印一个函数
var yaojin = '遥近'
function yaojin () {
console.log(1)
}
console.log(yaojin)// 打印遥近
可能有人看到这里会觉得奇怪,不是函数提升了吗,其实并不是,只是函数提升是整体的提升,它将定义的部分也提升上去了,所以在同名的变量的情况下,您可以理解成,函数的提升大于变量的提升
上面的代码其实就是这样
var yaojin = function () {
console.log(1)
}
console.log(yaojin)// 打印一个函数
yaojin = '遥近'
console.log(yaojin)// 打印遥近
- let和const定义的变量不存在变量的提升
转载自:https://juejin.cn/post/6926685759965511694