2021年前端面试javascript专区
一、javascript基础知识
1.1 基本类型和数据类型
基本类型:null、undefinded、number、string、boolean、symbol;
引用类型:Object、Array、Function、Date、RegExp;
除去上面的基本类型,其他的都是对象。基本类型没有附加方法; 所以你永远不会看到undefined.toString()。 也正因为如此,基本类型是不可变的,因为它们没有附加的方法可以改变它:
let s = "boy";
s.bar = "girl";
console.log(s.bar); // undefined
而默认情况下,对象是可变的,可以添加方法:
let obj = {};
obj.foo = 123;
console.log(obj.foo); // 123
与作为引用存储的对象不同,原始类型作为值本身存储。 这在执行相等性检查时会产生影响:
"dog" === "dog"; // true
14 === 14; // true
{} === {}; // false
[] === []; // false
(function () {}) === (function () {}); // false
三个基本类型string,number和boolean,它们有时被当做包装器类型,并且在原始值和包装类型之间进行转换很简单:
- 原始类型 to 包装类型: new String("abc");
- 包装类型 to 原始类型: new String("abc").valueOf();
比如字符串“abc”之类的原始值与new String(“abc”)之类的包装器实例有根本上的不同。 例如(用typeof和instanceof判断时):
typeof "pet"; //"string"
typeof new String("pet"); //"object"
"pet" instanceof String; // false
new String("pet") instanceof String; // true
"pet" === new String("pet"); // false
其实包装器实例就是一个对象,没办法在JavaScript中比较对象,甚至不能通过非严格相等 ==:
var a = new String("pet");
var b = new String("pet");
a == b; // false
a == a; // true
基本类型
- 基本类型的变量是存放在栈内存(Stack)里的;
- 基本数据类型的值是按值访问的;
- 基本类型的值是不可变的;
- 基本类型的比较是它们的值的比较;
引用类型
- 引用类型的值是保存在堆内存(Heap)中的对象(Object);
- 引用类型的值是按引用访问的;
- 引用类型的值是可变的;
- 引用类型的比较是引用的比较;
栈内存是自动分配内存的。而堆内存是动态分配(new的过程)内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为null(js垃圾回收),从而减少无用内存的消耗。
1.2 0.1+0.2>0.3
js中浮点数使用64位固定长度来表示,具体是按照IEEE754 这个标准来的,这个标准权衡了精度和表示范围,也就是如何有效利用这64个二进制数字的前提下提出的。下面的所有流程都是按这个标准来的,其中把64位划分出了3个区域:
- 区域 S 符号位 用 1 位表示 0表示正数 1表示负数;
- 区域 E 指数位 用 11 位表示 有正负范围,临界值是1023 后面看转换过程就能看明白;
- 区域 M 尾数位 用 52 位表示;
提示:
- 十进制的小数转换为二进制,主要是小数部分乘以2,取整数部分依次从左往右放在小数点后,直至小数点后为0。例如0.1,要转换为二进制的小数。
- 二进制的小数转换为十进制主要是乘以2的负次方,从小数点后开始,依次乘以2的负一次方,2的负二次方,2的负三次方等。
1). 而0.1转为二进制是一个无限循环数0.00011001100110011001100110011001100110011001100110011001100110011001......(1100循环)
取0
取0
取0
取1
取1
取0
...
2). 转换为指数格式其实就是移动小数点,让小数点前面出现的是第一个为1的值,不同的二进制数据,可能是前移可能是右移,对应的是指数的正负范围,转换后是这样子的:
1.1001100110011001100110011001100110011001100110011001100110011001.....
这里可以看到向右移动了4位,这个数据会保存在指数区域E内,在没有移位的情况下指数区域的值是1023,向左移动几位就加几位,向右移动几位就减几位,所以这里是:
1023 - 4 = 1019
1019 转二进制并补齐11位 01111111011
复制代码
也就是E为 01111111011,由于尾数位最多只有52位,所以小数点后面的52位全部提取到尾数位,其中要注意的是,类似四舍五入,如果末位后是1会产生进位,这里就产生了进位:
1001100110011001100110011001100110011001100110011001100110011001.....
1001100110011001100110011001100110011001100110011001 100110011001.....
进位后截取
1001100110011001100110011001100110011001100110011010
也就是M为 1001100110011001100110011001100110011001100110011010
这里由于丢掉了部分数据,所以导致精度丢失
3). 由于0.1是正数,所以 S 为 0。
接下来,我们看看 0.1 在内存中是长什么样子的:
let bytes = new Float64Array(1);// 64位浮点数
bytes[0] = 0.1;// 填充0.1进去
let view = new DataView(bytes.buffer);
console.log(view.getUint8(0).toString(2));// 10011010
console.log(view.getUint8(1).toString(2));// 10011001
console.log(view.getUint8(2).toString(2));// 10011001
console.log(view.getUint8(3).toString(2));// 10011001
console.log(view.getUint8(4).toString(2));// 10011001
console.log(view.getUint8(5).toString(2));// 10011001
console.log(view.getUint8(6).toString(2));// 10111001
console.log(view.getUint8(7).toString(2));// 00111111 这里补齐了8位
这里的bytes.buffer代表的就是一串内存空间,为了方便大家理解,这里使用 DataView用无符号8位的格式一个一个地读取内存的数据再转为二进制格式。 由于读取内存的顺序会受字节序的影响,可能在你们的电脑打印得到相反的顺序 如果按SEM的排列,那么其二进制就像下面这样子的:
s(0)E(01111111011)M(1001100110011001100110011001100110011001100110011010)。
把它存到内存中再取出来转换成十进制就不是原来的0.1
了,就变成了0.100000000000000005551115123126:
1). 提取尾数位数据:1001100110011001100110011001100110011001100110011010;
2). 先前添加 1, 恢复为指数格式,并提取指数位:
1.1001100110011001100110011001100110011001100110011010
01111111011 => 1019
1019 - 1023 = -4
3) 移位:
0.00011001100110011001100110011001100110011001100110011010
4). 二进制转化为十进制
0.00011001100110011001100110011001100110011001100110011010 =>
0.100000000000000005551115123126
02+0.1:
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004
那既然0.1不是0.1了,为什么在console.log(0.1)的时候还是0.1呢?
在console.log的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串。
1.3 0.2+0.3=0.5
// 0.2 和 0.3 都转化为二进制后再进行计算
0.001100110011001100110011001100110011001100110011001101 +
0.0100110011001100110011001100110011001100110011001101 =
0.10000000000000000000000000000000000000000000000000001
// 而实际取值只取52位尾数位,就变成了
0.1000000000000000000000000000000000000000000000000000 //0.5
0.2 和0.3分别转换为二进制进行计算:在内存中,它们的尾数位都是等于52位的,而他们相加必定大于52位,而他们相加又恰巧前52位尾数都是0,截取后恰好是0.1000000000000000000000000000000000000000000000000000也就是0.5
1.4 判断数据类型
1). typeof:返回数据类型,包含这7种: number、boolean、symbol、string、object、undefined、function。
- typeof null 返回类型错误,返回object;
- 针对引用类型,除了function返回function类型外,其他均返回object;
其中,null 有属于自己的数据类型 Null , 引用类型中的数组、日期、正则 也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,没有错,但不是我们想要的结果。
function getName() {
console.log("xxxx");
}
typeof undefined // 'undefined'
typeof '10' // 'string'
typeof 10 // 'number'
typeof false // 'boolean'
typeof Symbol() // 'symbol'
typeof Function // ‘function'
typeof null // ‘object’
typeof [] // 'object'
typeof {} // 'object'
typeof new getName() //作为构造函数创建 对象实例 类型object
typeof getName //function
为什么typeof null是Object:
因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object
这个bug是初版本的JavaScript中留下的,扩展一下其他五种标识位:
- 000 对象
- 1 整型
- 010 双精度类型
- 100字符串
- 110布尔类型
2). toString :toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
判断类型举例:
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
3). constructor:constructor是原型prototype的一个属性,当函数被定义时候,js引擎会为函数添加原型prototype,并且这个prototype中constructor属性指向函数引用, 因此重写prototype会丢失原来的constructor。
var d = new Number(1)
var e = 1
function fn() {
console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(e.constructor);//ƒ Number() { [native code] }
console.log(e.constructor.name);//Number
console.log(fn.constructor.name) // Function
console.log(date.constructor.name)// Date
console.log(arr.constructor.name) // Array
console.log(reg.constructor.name) // RegExp
null 和 undefined 无constructor,这种方法判断不了。
如果自定义对象,开发者重写prototype之后,原有的constructor会丢失,因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。
4). instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,只能判断对象是否存在于目标类型的原型链上:
由上图可以看出[]的原型(__proto__)指向Array.prototype,间接指向Object.prototype, 因此 [] instanceof Array 返回true, [] instanceof Object 也返回true。
function Foo() { };
var f1 = new Foo();
var d = new Number(1);
console.log(f1 instanceof Foo);// true
console.log(d instanceof Number); //true
console.log(123 instanceof Number); //false -->不能判断字面量的基本数据类型
自己实现:
function myInstance(L, R) {//L代表instanceof左边,R代表右边
var RP = R.prototype
var LP = L.__proto__
while (true) {
if (LP == null) {
return false
}
if (LP == RP) {
return true
}
LP = LP.__proto__
}
}
console.log(myInstance({}, Object));
1.5 == 和 ===
===
是严格意义上的相等,会比较两边的数据类型和值大小:
- 数据类型不同返回false;
- 数据类型相同,但值大小不同,返回false;
==
是非严格意义上的相等:
-
两边类型相同,比较大小;
-
两边类型不同,根据下方表格,再进一步进行比较;
- Null == Undefined ->true;
- String == Number ->先将String转为Number,再比较大小;
- Boolean == Number ->现将Boolean转为Number,再进行比较;
- Object == String,Number,Symbol -> Object 转化为原始类型;
1.6 call、apply、bind区别
通过call、apply以及bind方法改变this的行为,其中call与apply比较特殊,它们在修改this的同时还会直接执行方法,而bind只是返回一个修改完this的boundFunction并未执行.
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
let obj3 = {
name: 'echo'
}
var name = '行星飞行';
function fn() {
console.log(this.name);
};
fn(); //行星飞行 由于函数调用时前面并未指定任何对象,这种情况下this指向全局对象window。
fn.call(obj1); //听风是风
fn.apply(obj2); //时间跳跃
fn.bind(obj3)(); //echo
在js中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而call与apply让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用。
除了都能改变this指向并执行函数,call与apply唯一区别在于参数不同,具体如下:
var fn = function (arg1, arg2) {
// do something
};
fn.call(this, arg1, arg2); // 参数散列
fn.apply(this, [arg1, arg2]) // 参数使用数组包裹
call第一参数为this指向,后续散列参数均为函数调用所需形参,而在apply中这些参数被包裹在一个数组中。
call实用案例:
function type(obj) {
var regexp = /\s(\w+)\]/;
var result = regexp.exec(Object.prototype.toString.call(obj))[1];
return result;
};
console.log(type([123]));//Array
console.log(type('123'));//String
console.log(type(123));//Number
console.log(type(null));//Null
console.log(type(undefined));//Undefined
注意,如果在使用call之类的方法改变this指向时,指向参数提供的是null或者undefined,那么 this 将指向全局对象。
1.7 实现一个call
我们从一个简单的例子解析call方法:
var name = '时间跳跃'; // 不可以使用let、var全局变量
let obj = {
name: '听风是风'
};
function fn() {
console.log(this.name);
};
fn(); //时间跳跃
fn.call(obj); //听风是风
在这个例子中,call方法主要做了两件事:
- 修改了this指向,指向obj,比如fn()默认指向window,所以输出时间跳跃;
- 执行了函数fn;
先说第一步改变this怎么实现,其实很简单,只要将方法fn添加成对象obj的属性不就好了。所以我们可以这样:
Function.prototype.myCall = function (obj) {
// 不传递默认为window
obj = obj || window;
// 此时this就是函数fn,即myCall的调用者
obj.fn = this;
// 保存参数
let args = Array.from(arguments).slice(1) //Array.from 把伪数组对象转为数组
// 调用函数
let result = obj.fn(...args);
delete obj.fn
return result
}
其中arguments为函数默认属性,代指函数接收的所有参数,它是一个伪数组。
测试代码:
fn.myCall(obj, "我的", "名字", "是"); // 我的名字是听风是风
fn.myCall(null, "我的", "名字", "是"); // 我的名字是时间跳跃
fn.myCall(undefined, "我的", "名字", "是"); // 我的名字是时间跳跃
1.8.实现一个apply方法
apply方法因为接受的参数是一个数组,所以模拟起来就更简单了,理解了call实现,我们就直接上代码:
Function.prototype.myApply = function (obj) {
// 不传递默认为window
obj = obj || window;
// 此时this就是函数fn,即myCall的调用者
obj.fn = this;
// 是否传参
if (arguments[1]) {
result = obj.fn(...arguments[1])
} else {
result = obj.fn()
}
delete obj.fn
return result
}
测试代码:
fn.myApply(obj, ["我的", "名字", "是"]); // 我的名字是听风是风
fn.myApply(null, ["我的", "名字", "是"]); // 我的名字是时间跳跃
fn.myApply(undefined, ["我的", "名字", "是"]); // 我的名字是时间跳跃
js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解
1.9 实现一个bind
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
说的通俗一点,bind与apply/call一样都能改变函数this指向,但bind并不会立即执行函数,而是返回一个绑定了this的新函数,你需要再次调用此函数才能达到最终执行。
此外:返回的bind函数除了支持不普通调用,还支持new调用,这两者this指向不同,new的this指向创建的实例对象,直接调用this指向我们绑定的对象。
我们知道(强行让你们知道)构造函数实例的constructor属性永远指向构造函数本身,比如:
function Fn(){};
var o = new Fn();
console.log(o.constructor === Fn);//true
而构造函数在运行时,函数内部this指向实例,所以this的constructor也指向构造函数:
function Fn() {
console.log(this.constructor === Fn); //true
};
var o = new Fn();
console.log(o.constructor === Fn); //true
所以我就用constructor属性来判断当前bound方法调用方式,毕竟只要是new调用,this.constructor === Fn
一定为true。
Function.prototype.myBind = function (obj) {
// 判断调用者是不是函数
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
};
//第0位是this,所以得从第一位开始裁剪
let args = Array.prototype.slice.call(arguments, 1);
let fn = this;
let bound = function () {
//二次调用我们也抓取arguments对象
let params = Array.prototype.slice.call(arguments);
//通过constructor判断调用方式,为true this指向实例,否则为obj
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
};
//原型链继承
bound.prototype = fn.prototype;
return bound;
}
测试代码:
var z = 0;
let obj = {
z: 1
};
function fn(x, y) {
this.name = '听风是风';
console.log(this.z);
console.log(x);
console.log(y);
};
fn.prototype.age = 26;
let bound = fn.myBind(obj, 2);
bound(3); // this指向Obj 1 2 3
let person = new bound(3); //undefined 2 3 new的this指向创建的实例对象
console.log(person.name); //听风是风
console.log(person.age); //26
person.__proto__.age = 18; // 修改原型
person = new fn(); // new的this指向创建的实例对象 undefined undefined undefined
console.log(person.age); //18.
1.10 js创建对象的方式
- 使用内置对象;
- 使用json符号;
- 自定义对象构造;
1) 使用内置对象
js可用的内置对象分为两种:
- js语言原生对象:Function、Object、Date等;
- JavaScript运行期的宿主对象(环境宿主级对象),如window、document、body等。
我们所说的使用内置对象,是指通过JavaScript语言原生对象的构造方法,实例化出一个新的对象。如:
let str = new String("实例初始化String");
let func = new Function("x","alert(x)");//示例初始化func
let o = new Object();//示例初始化一个Object
console.log(str instanceof Object); // true
console.log(func instanceof Object); // true
console.log(o instanceof Object); // true
2) 使用json符号
在JavaScript中,json被理解为对象。通过字符串形式的json,数据可以很方便地解析成JavaScript独享,并进行数据的读取传递。
例如:
let json = {
book:[
{name:"三国演义"},
{name:"西游记"},
{name:"水浒传"},
{name:"红楼梦"}
],
author:[
{name:"罗贯中"},
{name:"吴承恩"},
{name:"施耐安"},
{name:"曹雪芹"}
],
sayHello:()=>{
console.log('Hello world');
}
};
json.sayHello();
3) 自定义对象构造
创建高级对象构造有两种方式:使用“this”关键字构造、使用原型prototype构造。如:
//使用this关键字定义构造的上下文属性
function Girl() {
this.name = "big pig";
this.age = 20;
this.standing;
this.bust;
this.waist;
this.hip;
}
//使用prototype
function Girl() { }
Girl.prototype.name = "big pig";
Girl.prototype.age = 20;
Girl.prototype.standing;
Girl.prototype.bust;
Girl.prototype.waist;
Girl.prototype.hip;
alert(new Girl().name);
上例中的两种定义在本质上没有区别,都是定义“Girl”对象的属性信息。“this”与“prototype”的区别主要在于属性访问的顺序。如:
function Test() {
this.test = function () {
alert("defined by this");
}
}
Test.prototype.test = function () {
alert("defined by prototype");
}
var _o = new Test();
_o.test();//输出“defined by this”
当访问对象的属性或者方法是,将按照搜索原型链prototype chain的规则进行。首先查找自身的静态属性、方法,继而查找构造上下文的可访问属性、方法,最后查找构造的原型链。
“this”与“prototype”定义的另一个不同点是属性的占用空间不同:
- 使用“this”关键字,实例初始化时为每个实例开辟构造方法所包含的所有属性、方法所需的空间;
- 而使用“prototype”定义,由于“prototype”实际上是指向父级的一种引用,仅仅是个数据的副本,因此在初始化及存储上都比“this”节约资源。
1.11 函数和对象区别
每一个javascript函数都表示为一个对象,更确切的数,是Function对象的一个实例。
let Parent = function(name,age){
this.name = name;
this.age = age;
}
console.log(Parent instanceof Object);
console.log(Parent instanceof Function);
console.log(typeof Parent);
function Test (word) {
console.log (word);
console.log(this.constructor === Test);
}
Test('我是函数'); //我是函数 false
new Test('我是对象'); // 我是对象 true
//将以上的调用方式换种通俗易懂的方式
Test.call(); //相当于Test(); undefined false
//相当于new Test();
let obj = {};
obj._proto_ = Test.prototype;
Test.call(obj); // undefined false(this为obj)
函数直接调用和new 实例对象,两次调用之中的this不同。调用Test('...');的时候,里面的this是顶级对象window,返回值是undefined。调用new Test('...');的时候,它会先new一个对象,置类型为Test,之后把它作为this执行Test函数,最后再把对象返回。
1.12 字面量创建对象和new创建对象有什么区别
我们先来说以下字面量,也就是使用json创建对象,这种方式创建对象:
- 字面量创建对象简单,方便阅读;
- 不需要作用域解析,速度更快(没有this);
我们再来说一下new,下面的例子中分别通过构造函数与class类实现了一个简单的创建实例对象的过程。
// ES5构造方法
let Parent = function(name,age){
this.name = name;
this.age = age;
}
// 给原型添加方法
Parent.prototype.sayHello = function(){
console.log(this.name);
}
let child = new Parent('听风是雨',26);
child.sayHello();
//ES6 class类
class CParent{
// 构造方法
constructor(name,age){
this.name = name;
this.age = age;
}
sayHello(){
console.log(this.name);
}
}
let cChild = new CParent('听风是雨',26);
cChild.sayHello();
那么new到底做了什么?我们如何实现一个new。
比较直观的感觉,当我们new一个构造函数,得到的实例继承了构造器的构造属性(this.name这些)以及原型上的属性。
new的过程说的比较直白,当我们new一个构造器,主要有三步:
- 创建一个新对象;
- 使新对象的__proto__指向原函数的prototype;
- 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result;
- 判断执行函数的结果是不是null或undefined,如果是则返回之前的新对象,如果不是则返回result;
function myNew(fn, ...args){
let obj = {};
// 指向fn原型
obj.__proto__ = fn.prototype;
// fn内部this指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj;
}
// ES5构造方法
let Parent = function(name,age){
this.name = name;
this.age = age;
}
// 给原型添加方法
Parent.prototype.sayHello = function(){
console.log(this.name);
}
let child = myNew(Parent,'听风是雨',26);
child.sayHello();
1.13 字面量new出来的对象和 Object.create(null)创建出来的对象有什么区别
字面量和new创建出来的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型;
而 Object.create(null)创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性;
二、javascript作用域
作用域概念是理解javascrip的关键所在,不仅仅从西能角度,还包括从功能的角度。作用域对javascript有许多影响,从确定哪些变量可以被函数访问,到确定this的赋值。
2.1 作用域
通俗的讲:变量和函数的可使用范围称作作用域。在Javascript 中,作用域分为全局作用域 和 函数作用域。
- 全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。
- 函数作用域:在固定的代码片段才能被访问。
作用域有上下级关系,上下级关系的确定就看函数是在哪个作用域下创建的。如上,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
变量取值:到创建 这个变量的函数的作用域中取值。
2.2 作用域链
一般情况下,变量取值到创建这个变量的函数的作用域中取值。
但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
2.3 执行上下文
执行上下文分为:
- 全局执行上下文:
- 创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出;
- 函数执行上下文:eval执行上下文;
- 每次函数调用时,都会新创建一个函数执行上下文;
- 执行上下文分为创建阶段和执行阶段;
- 创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域;
- 执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象;
2.4 执行栈
- 首先栈特点:先进后出;
- 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行出栈;
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文;
- 只有浏览器关闭的时候全局执行上下文才会弹出;
2.5 闭包
闭包是javascript最强大的特性之一,它允许函数访问局部作用域之外的数据。函数和函数内部能访问到的变量的总和,就是一个闭包。
闭包常用来间接访问一个变量,换句话说,用来隐藏一个变量。类似强类型语言中的private,这样可以使私有变量不受外界干扰,起到保护作用。
下面演示一个demo:
function foo(){
let number = 1
function increment(){ // 访问局部作用域之外的数据number
number++
return number
}
return increment;
}
let increment = foo();
console.log(increment()); //2
console.log(increment()); //3
上面例子number和increment函数组成了一个闭包。这里将number放到一个函数里,可以起到隐藏变量的作用。
应用:
- 设计模式中的单例模式;
- for循环中的保留i的操作;
- 防抖和节流;
- 函数柯里化;
三 javascript原型
3.1 原型
原型分为隐式原型和显式原型,每个实例对象都有一个隐式原型,它指向自己的构造函数的显式原型。
function Person(name){
this.name = name;
}
// 创建实例对象
let person= new Person("ce");
// 变量实例对象隐式原型 === 构造函数的显式原型
console.log(person.__proto__ === Person.prototype); //true
console.log(Person === Person.prototype.constructor); //true
js函数提供了一个属性prototype,函数的这个原型指向一个对象,这个对象叫原型对象。这个原型对象有一个constructor属性,指向这个函数本身。
js实例对象提供了一个属性__proto__,一般也被称作隐身原型,它指向自己的构造函数的显式原型。
文字描述往往没有图像描述来的直观,那么我们用一个三者之间的关系图来直观的了解。
原型作用:
- 原型作用之一:多个实例对象共享一个原型对象,节省内存空间;
- 原型作用之二:实现继承;
3.2 原型实现继承
比如,现在有一个"动物"对象的构造函数。
function Animal(){
this.specis = "动物";
}
还有一个"猫"对象的构造函数。
function Cat(name,color){
this.anme = name;
this.color = color;
}
怎样才能使"猫"继承"动物"呢? 如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。
Cat.prototype = new Animal(); // 原型对象 子类共享父类所有属性,不能传参
Cat.prototype.constructor = Cat;
let cat = new Cat("大猫","yellow");
console.log(cat.specis);
3.3 原型链
多个__proto__组成的继承关系成为原型链。
- 所有实例对象的__proto__都指向他们构造函数的prototype;
- 所有构造函数prototype都是对象,自然它的__proto__指向的是Object;
- 所有的构造函数的隐式原型指向的都是Function的显示原型;
- Object的隐式原型是null;
四、javascript中级知识
4.1 js中的常用的继承方式有哪些?以及各个继承方式的优缺点
function Animal(){
this.specis = "动物";
}
function Cat(name,color){
this.anme = name;
this.color = color;
}
Cat.prototype = new Animal(); // 修改原型对象 子类共享父类所有属性,不能传参
Cat.prototype.constructor = Cat;
let cat = new Cat("大猫","yellow");
console.log(cat.specis); //动物
console.log(JSON.stringify(cat)); //{"anme":"大猫","color":"yellow"}
2). 构造函数继承:
function Parent(name,age){
this.name = name;
this.age = age;
}
Parent.prototype.sex = '男';
function Child(phone,name,age){
// 调用父类构造函数
Parent.call(this,name,age);
this.phone = phone;
}
let ins = new Child('181','zy','26');
console.log(JSON.stringify(ins)); //{"name":"zy","age":"26","phone":"181"}
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了