程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

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垃圾回收),从而减少无用内存的消耗。

前端面试:谈谈 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.12=0.2 取0

0.22=0.4 取0

0.42=0.8 取0

0.82=1.6 取1

0.62=1.2 取1

0.22=1.4 取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的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串。

js浮点数存储精度丢失原理

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)); 
复制代码

 js的原型和原型链

「每日一题」什么是 JS 原型链?

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绑定、箭头函数绑定详解

js 实现call和apply方法,超详细思路分析

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.
复制代码

js 手动实现bind方法,超详细思路分析!

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

JavaScript中函数和对象的区别

复制代码
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的操作;
  • 防抖和节流;
  • 函数柯里化;

「每日一题」JS 中的闭包是什么?

三 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__,一般也被称作隐身原型,它指向自己的构造函数的显式原型。

文字描述往往没有图像描述来的直观,那么我们用一个三者之间的关系图来直观的了解。

原型作用:

  • 原型作用之一:多个实例对象共享一个原型对象,节省内存空间;
  • 原型作用之二:实现继承;

什么是Js原型?(1)(包括作用:继承)

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中的常用的继承方式有哪些?以及各个继承方式的优缺点

1). 原型链继承:
复制代码
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-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
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-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(145)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示