前言: HTML 和 CSS 相关的面试题请阅读HTML + CSS 面试题
JS数据类型有关的之前已经写过了,需要的请阅读JS基本数据类型与引用数据类型
、数据类型判断的四种方法
JS作用域和作用域链
作用域: 作用域决定了代码区块中变量和其他资源的可见性,不同
作用域下同名变量
不会有冲突。
全局作用域
- 在页面打开时被创建,页面关闭时被销毁
- 编写在
script
标签中的变量和函数,在页面任意位置
都能访问 - 全局作用域中有全局对象
window
,代表一个浏览器窗口,由浏览器创建,可以直接调用 - 全局作用域中声明的变量和函数会作为
window
的对象属性和方法保存
函数作用域
- js 采用的是
静态作用域
,所以函数的作用域在函数定义
时就确定了 - 相互独立,
每调用一次创建一个函数作用域
- 函数作用域内
可以
访问到全局作用域中的变量,函数外无法访问
函数内部的局部变量
块级作用域
- 块级作用域可通过新增命令
let
和const
声明,所声明的变量在指定块的作用域外无法
被访问。块级作用域在如下情况被创建:- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
- 声明变量
不会提升
到代码块顶部 禁止重复声明
在自身作用域中寻找某个变量,若找不到,会到当前作用域的上一级作用域内查找,直到全局作用域,这种层级关系就叫作用域链。
在ES6之前,JavaScript没有块级作用域,只有函数作用域和全局作用域,ES6之后可以通过 let 和 const 体现。
前往 var、let 、const 阅读
推荐文章:JavaScript深入之词法作用域和动态作用域和深入理解JavaScript作用域和作用域链
原型和原型链
原型和原型链,需要理解了下面这张图和代码,并且能默画出这张图。
function Foo() {}
let f1 = new Foo();
let f2 = new Foo();
推荐文章:JavaScript深入之从原型到原型链、轻松理解JS 原型原型链
闭包
闭包: 定义在函数内部的函数,并且使用了外部函数内的局部变量
闭包产生的条件:
- 函数嵌套
- 内部函数引用了外部函数内的局部变量
闭包的作用:
- 延长变量的生命周期
- 创建私有环境
闭包的优缺点:
- 优点
- 执行空间不销毁,变量也没有销毁
- 可以读取函数内部的变量
- 让这些变量的值始终保存在内存
- 缺点
- 变量一直存储在内存中,内存消耗大,容易造成内存泄露
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
推荐文章:JavaScript深入之闭包
call、apply、bind 的实现
call
简单来说,call 调用函数方法就是用来改变函数内 this 的指向
举个例子:
function person(){
console.log(this.name)
}
const obj = {
name: "aaa",
}
person.call(obj) // 打印的 name 的值是 aaa 这里的 this 指向 obj 对象
person() // undefined 这里的 this 由 window 调用,指向 window
call 的实现
Function.prototype.newCall = function (obj){
var obj = obj || window; // 这是为了防止传入的第一个参数为 null 时输出报错
// 原本this指向person函数,给obj添加一个p属性,赋值为this,person函数在obj内被调用,this指向obj,从而改变person函数内this的指向
obj.p = this;
// 调用this方法
obj.p();
// 保存传入的其他参数
var newArguments = [];
for (var i = 1; i < arguments.length; i++){
newArguments.push('arguments[' + i + ']');
}
var result = eval('obj.p(' + newArguments + ')');
// 原本obj内没有p这个属性,调用之后删除p这个属性
delete obj.p;
return result;
}
apply
apply 与 call 类似,最大的区别就是 apply 接收的第二个参数为数组
,而 call 从第二个参数开始接收单个参数
。
person.call(obj,1,2,3)
person.apply(obj,[1,2,3])
因此 apply 的实现如下:
Function.prototype.newApply = function (obj,arr){
var obj = obj || window; // 这是为了防止传入的第一个参数为 null 时输出报错
var result;
obj.p = this;
// 如果数组为空,直接返回
if( !arr ){
result = obj.p();
}else{
// 若数组不为空,则与 call 实现方法类似
var newArguments = [];
for (var i = 0; i < arguments.length; i++){
newArguments.push('arr[' + i + ']');
}
result = eval('obj.p(' + newArguments + ')');
}
delete obj.p;
return result;
}
bind
bind 与 call、apply 的区别就是 bind 会返回一个函数,并且不会立即执行。
fn.call(obj,1) // 会立即执行并返回值
fn.bind(obj,1) // 绑定了this的指向但不会立即执行,返回的是一个函数,需要再次调用这个函数才会执行
bind 的实现
Function.prototype.newBind = function (obj,arr){
// 使用闭包保存this,此时this指向person
var that = this;
var arr = Array.prototype.slice.call(arguments,1);
return function(){
var arr2 = Array.prototype.slice.call(arguments);
// 合并两个函数接收的参数
var arrsum = arr.concat(arr2)
that.call(obj,arrsum);
}
}
同时,bind 可以实现 new 操作,但此时的 this 会失效,我们用原生的 bind 尝试一下:
function person(a,b,c){
console.log(this.name)
console.log(a,b,c)
}
const obj = {
name: "aaa",
}
var P = person.bind(obj,1,2);
var p = new P(3); // 此时打印出来的值为 undefined 1 2 3 可以看到 this 失效了
再实现 bind 的 new 操作,此时 bind 的实现变成了这样:
function person(a,b,c){
console.log(this.name)
console.log(a,b,c)
}
// 给person的原型添加一个属性
person.prototype.s = "sss"
const obj = {
name: "aaa",
}
Function.prototype.newBind = function (obj,arr){
// 使用闭包保存this,此时this指向person
var that = this;
var arr = Array.prototype.slice.call(arguments,1);
return function(){
var arr2 = Array.prototype.slice.call(arguments);
// 合并两个函数接收的参数
var arrsum = arr.concat(arr2)
that.call(obj,arrsum);
}
}
// 再使用刚刚写的 newBind 尝试调用原型对象中的 s 属性
var P = person.newBind(obj,1,2);
var p = new P(3); // aaa 1 2 3 新的 newBind 方法没有实现 new 的操作,this 还是指向 obj
console.log(p.s) // undefined 发现此时实例对象与原型对象没有关系
// 重新实现 new 效果以及原型对象的串联
Function.prototype.newBind = function (obj,arr){
// 使用闭包保存this,此时this指向person
var that = this,
arr = Array.prototype.slice.call(arguments,1),
o = function(){},
newf = function(){
// console.log( this instanceof newf) // true 发现此时的this是newf的实例,也就是说this是指向new 的实例 p
var arr2 = Array.prototype.slice.call(arguments),
// 合并两个函数接收的参数
arrsum = arr.concat(arr2)
// 判断有没有进行new操作,如果有,则把this绑定到new的实例上,否则绑定到obj
if(this instanceof newf){
that.call(this,arrsum)
}else{
that.call(obj,arrsum);
}
}
// 利用一个空函数把原型对象串联起来
o.prototype = that.prototype
newf.prototype = new o;
return newf;
}