InterviewMap —— Javascript (一)

308 阅读14分钟

我的前端面试准备合集

本人是在校大学生,准备面试了,记录一些基础知识吧~
- 目录
- 认识js
- 数据类型
- 类型转换
- 运算符

一、认识Javascript

javascript是一种轻量级的脚本语言,所以它不具备开发操作系统的能li,只是用来操作控制一些大型的应用程序的一种嵌入式的脚本。

javascript语法很简单,大致分为基本语法(比如操作符、控制结构、语句)和标准库,就是一系列具有各种功能的对象比如Array、Date、Math等),再者javascript提供的api很少,I/O等相关API都是靠宿主环境,去调用宿主环境的API的。

目前,已经嵌入 JavaScript 的宿主环境有多种,最常见的环境就是浏览器,另外还有服务器环境,也就是 Node 项目。

从语法上看js是一门对象模型的语言,但是却又不是一门真正的面向对象的语言,因为除此之外,他还支持编程范式,比如函数式编程。

JavaScript 的发明目的,就是作为浏览器的内置脚本语言,为网页开发者提供操控浏览器的能力。它是目前唯一一种通用的浏览器脚本语言,所有浏览器都支持。它可以让网页呈现各种特殊效果,为用户提供良好的互动体验。

Javascript诞生记

二、数据类型

1、数据类型的认识

Js内置了7种数据类型,七种内置类型又分为两大类型:基本类型和对象(Object)。

除了symbol 不常用之外,包括五种基本类型null、undefined、string、number、boolean和一种引用类型object。ES6引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型。它也是基本数据类型。

javascript在设计当初借鉴了java的一些东西,我们可以对比他门的数据类型,可以发现许多相似之处,只是js将多个类型归结为一个了。

其中 JS 的 number 类型是浮点类型的,在使用中会遇到某些Bug,比如0.1 + 0.2 !== 0.3,但是这一块的内容会在进阶部分讲到。string 类型是不可变的,无论你在 string 类型上调用何种方法,都不会对值有改变。

为什么JS可以一个number解决所有的数值类型,或者说一个变量就可以承载任意类型的数据。答案在你开始学习JavaScript的那几段话中,“JavaScript是一种动态类型、弱类型...的语言”。动态类型意味着在声明一个变量之后,可以存储不同类型的变量。也就是说在JS中声明一个变量a,他的初始值是13,var a= 13, 在这语句之后,你仍然可以再写一个这样的语句a = '13.333' 来改变a的值且不会报错,甚至可以写 a = 'name is sam' 。

  • 按值访问和按引用访问
基本数据类型的值是按值访问的
基本类型的值是不可变的
基本类型的比较是它们的值的比较
基本类型的变量是存放在栈内存(Stack)里的

引用类型的值是按引用访问的
引用类型的值是可变的
引用类型的比较是引用的比较
引用类型的值是保存在堆内存(Heap)中的对象(Object) 与其他编程语言不同,JavaScript 不能直接操作对象的内存空间(堆内存)

图解:

  • 针对按值访问和按引用访问的相等判断~~

对于引用类型的变量,==和===只会判断引用的地址是否相同,而不会判断对象具体里属性以及值是否相同。因此,如果两个变量指向相同的对象,则返回true。

var arrRef = ["Hi!"];
var arrRef2 = arrRef;

console.log(arrRef === arrRef2); // true

如果是不同的对象,及时包含相同的属性和值,也会返回false。

var arr1 = ["Hi!"];
var arr2 = ["Hi!"];

console.log(arr1 === arr2); // false

如果想判断两个不同的对象是否真的相同,一个简单的方法就是将它们转换为字符串然后判断。

var arr1str = JSON.stringify(arr1);
var arr2str = JSON.stringify(arr2);

console.log(arr1str === arr2str); // true

另一个方法就是递归地判断每一个属性的值,直到基本类型位置,然后判断是否相同。

  • 函数参数传递中的值传递和引用传递

当将基本数据类型传入函数,函数会进行拷贝副本然后赋值(重新开辟栈空间)。

var hundred = 100;
var two = 2;
function multiply(x, y) {
    return x * y;
}
var twoHundred = multiply(hundred, two);

hundred的值拷贝给变量x,two的值拷贝给变量y。

当传入的是引用类型,函数会拷贝其地址一份赋给参数变量,指向的其实是同一块堆地址,函数中的修改会直接改变外部的引用。

function changeAgeImpure(person) {
    person.age = 25;
    return person;
}
var alex = {
    name: "Alex",
    age: 30
};
var changedAlex = changeAgeImpure(alex);
console.log(alex); // { name: 'Alex', age: 25 }
console.log(changedAlex); // { name: 'Alex', age: 25 }

对该变量的操作将会影响到原本的对象。这样的编程手法将产生附带影响,是的代码的逻辑复杂和可读性变低。

因此,很多数组函数,比如Array.mapArray.filter是以纯函数的形式实现。虽然它们的参数是一个数组变量,但是通过深度拷贝并赋值给一个新的变量,然后在新的数组上操作,来防止原始数组被更改。

让我们来看如何实现一个纯函数:

function changeAgePure(person) {
    var newPersonObj = JSON.parse(JSON.stringify(person));
    newPersonObj.age = 25;
    return newPersonObj;
}
var alex = {
    name: "Alex",
    age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // { name: 'Alex', age: 30 }
console.log(alexChanged); // { name: 'Alex', age: 25 }

面试题目:值传递和引用传递经常在面试中被问到,来尝试回答一下如下代码如何输出:

function changeAgeAndReference(person) {
    // person是地址副本 person, personObj1 ---> 001
    person.age = 25; // 更改了外面的personObj1指向的变量对象的值
    // 然后让person 变量指向了另外一个地方
    person = {
        name: "John",
        age: 50
    };
    // 此时 person --> 002
    // personObj1 ---> 001
    return person; // 
}
var personObj1 = {
    name: "Alex",
    age: 30
};
var personObj2 = changeAgeAndReference(personObj1);
// personObj2, person ---> 002
console.log(personObj1); // -> 001 {name: "Alex", age: 25}
console.log(personObj2); // -> 002 {name: "John", age: 50}

面试题目:对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?

对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。

var a = 1;
// 为a分配一个栈内存
var b = 2;
// 为b分配一块栈内存,存储的内容与a相同
a = "meils";
// 为a再重新分配一个栈内存,之前的a的栈内存销毁。

接下来我们来看函数参数是对象的情况:

案例1

function test(person) {
  // 函数传参是传递对象指针的副本
  // 这里修改对象的属性,外面也同步
  person.age = 26
}


var p1 = {
  name: 'yck',
  age: 25
}
test(p1)
console.log(p1) 

// {name: "yck", age: 26}

案例2

function test(person) {
  person.age = 26
  person = {
	name: "zjj"
}
}


var p1 = {
  name: 'yck',
  age: 25
}
test(p1)
console.log(p1)

// {name: "yck", age: 26}

案例3

function test(person) {
  person.age = 26
  person = {
    name: 'yyy',
    age: 30
  }

  return person
}
var p1 = {
  name: 'yck',
  age: 25
}
var p2 = test(p1)
console.log(p1)  // {name: "yck", age: 26}
console.log(p2)  // {name: "yyy", age: 30}

接下来让我为你解析一番:

首先,函数传参是传递对象指针的副本
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图

数据类型解读

下面我们分别认识它们吧:

number类型我们需要知道哪些?

1、在js内部,所有的数值都是用64位浮点形式表示的,整数也是。
2、有些时候我们必须要使用整数来计算,这时候js会将64位小数转换位32位整数,然后再计算。
3、如果涉及到小数的比较运算,因为浮点型是不准确的,所以我们要小心。比如:
    0.1 + 0.2 === 0.3
    // false
    0.3 / 0.1
    // 2.9999999999999996
    (0.3 - 0.2) === (0.2 - 0.1)
    // false
4、第1位:符号位,0表示正数,1表示负数,11位指数部分,52位小数部分(即有效数字)。因此所能表示的整数范围是2^52

564位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为2^10242^1023(开区间),超出这个范围的数无法表示。
6、JavaScript 提供Number对象的MAX_VALUE和MIN_VALUE属性,返回可以表示的具体的最大值和最小值。
7、js特殊数值有 -0 +0 NaN 正负Infinity8、溢出能够表示的范围会返回Infinity(正数)或者0(负数)
9、数值相关的全局方法 parseInt() parseFloat() isNaN() isFinite()
10-0 === +0 // true
    0 === -0 // true
    0 === +0 // true
11、NAN 是非数值,主要出现在数字解析出错的场合,比如:
    5 - 'x' // NaN 
    0 / 0 // NaN
    typeof NaN // 'number'
    NaN === NaN // false
    Boolean(NaN) // false
    NaN + 32 // NaN
    NaN - 32 // NaN
    NaN * 32 // NaN
    NaN / 32 // NaN
    1 / -0 // -Infinity
    -1 / -0 // Infinity

string

1、JavaScript使用Unicode字符集。

null && undefined

1、 都表示没有,在真值判断中都是false,null == undefined
2、 二者的区别,null是一个表示‘空’的对象,转为数值时为0,undefined 是一个表示此处无定义的原始值,转为数值时为NaN。
3、 由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0,typeof null就错误的返回了"object"  

boolean

1、 除了以下值: undefined null false 0 NaN "" 或者' '其他值都视为true。
2、 空数组和空对象都是true

总之,6大基本类型一个引用类型,一共7大类型!!

对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型。

原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString()

此时你肯定会有疑问,这不对呀,明明 '1'.toString() 是可以使用的。其实在这种情况下,'1'已经不是原始类型了,而是被强制转换成了 String 类型也就是对象类型,所以可以调用 toString 函数。

let a = 111 // 这只是字面量,不是 number 类型
a.toString() // 使用时候才会转换为对象类型

2、数据类型的判别

typeof

适合判断基本数据类型和function的判断

有一点不足的是,typeof null == "object"

typeof "sss" ===> "string"
typeof 123 ===> "Number"
typeof [1,2,3] ===> "object"
typeof new Date() ===> "object"
typeof function(){alert('111');}  ===> "function"
typeof undefined ===> "undefined"
typeof NaN ===> "number"
typeof null ===> "object"

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

instanceof

判断已知的对象类型或者是自定义的对象

[1,3] instanceof Array === true
new Array('1, 3,4') instanceof Array === true
new Object() instanceof Object === true
new String("string") instanceof String === true
function(){this.name="22";} instanceof Function === true

// 定义一个构造函数
function Person(){
    
}
// 定义一个构造函数
function Student(){

}
// 每一个构造函数都有一个prototype对象属性, 这个对象属性将会作为通过new Person()创建的对象的一个原型。
// 也就是当我们在new 一个对象的时候,这个对象的原型就指向了这个构造函数的prototype。

Student.prototype = new Person(); // student继承至person

var bson = new Student();
bson instanceof Student
// false

bson instanceof Person
// true
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str = 'hello world'
str instanceof String // false

var str1 = new String('hello world')
str1 instanceof String // true

Object.prototype.toString.apply()

判断基本数据类型和内置对象

Object.prototype.toString.apply([]) === "[object Array]"
Object.prototype.toString.apply(function(){}) === "[object Function]"

Object.prototype.toString.apply(new Function); // "[object Function]"
Object.prototype.toString.apply(new Object);       // "[object Object]"
Object.prototype.toString.apply(new Date);         // "[object Date]"
Object.prototype.toString.apply(new Array);        // "[object Array]"
Object.prototype.toString.apply(new RegExp);       // "[object RegExp]"
Object.prototype.toString.apply(new ArrayBuffer);  // "[object ArrayBuffer]"
Object.prototype.toString.apply(Math);             // "[object Math]"
Object.prototype.toString.apply(JSON);             // "[object JSON]"
var promise = new Promise(function(resolve, reject) {
    resolve();
});
Object.prototype.toString.apply(promise);          // "[object Promise]"
Object.prototype.toString.apply(124)
// "[object Number]"
Object.prototype.toString.apply("222")
// "[object String]"
Object.prototype.toString.apply(true)
// "[object Boolean]"
Object.prototype.toString.apply(null)
// "[object Null]"
Object.prototype.toString.apply(null) === "[object Null]"   // 在IE6/7/8下存在有兼容性问题

如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。

2、类型转换

类型转换在平时会经常使用到,无非就是转换为布尔值、数值、字符串三类。

1、将非数值转换为数值类型的函数【非数值 => 数值】

Number()可以用于将任何类型转换为对应的数值。ParseInt()、ParseFloat()则是把字符串转为数值型。

var num = ["123" , "124.4" , "234asd" , "asf456"] ;

for (i = 0; i < num.length; i++) {
    console.log(parseInt(num[i]));
}
//  123 , 124 , 234 , NaN 

ParseFloat 远离同上。另外ParseInt还有另一项用途:parseInt(string,radix):以radix为基底,将string转换成多少进制的整数。

2、将其它类型的数据转换为字符串类型的函数【其他类型 => 字符串】

有两个函数可以把其他数据类型转换为字符串。toString() 和 string() 。

string(): 可以将任意类型转为字符型。 demo.toString(): 将demo转换成字符串类型。demo不能等于null undefined.(也可以传入进制)

3、将其他类型转换成布尔值类型【其他类型 => boolean类型】

1、 除了以下值: undefined null false 0 NaN "" 或者' '其他值都视为true。
2、 空数组和空对象都是true

4、对象转基本类型

对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:

如果已经是原始类型了,那就不需要转换了 调用 x.valueOf(),如果转换为基础类型,就返回转换的值 调用 x.toString(),如果转换为基础类型,就返回转换的值 如果都没有返回原始类型,就会报错 当然你也可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。

let a = {
  valueOf() {
  // 优先级第二
    return 0
  },
  toString() {
  // 优先级第三
    return '1'
  },
  [Symbol.toPrimitive]() {
    // 优先级最高
    return 2
  }
}

// a是一个对象,要想参与运算就必须先转为数值
1 + a // => 3
let a = {
  toString() {
    return '1'
  }
}
a + 1;
// "11"
let a = {
  valueOf() {
  // 优先级第二
    return 0
  },
  toString() {
  // 优先级第三
    return '1'
  }
}

1 + a // => 1

5、四则运算

1、只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。
2、对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字


1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

'a' + + 'b' // -> "aNaN"
// 因为 + 'b' -> NaN
// 你也许在一些代码中看到过 + '1' -> 1

true + true // 2
4 + [1,2,3] // "41,2,3"
// 数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3

'a' + + 'a'; // "aNaN"
// 因为 + 'b' 等于 NaN,所以结果为 "aNaN"

4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN 
Number([1]) // 1
Number([1,2]) // NaN
Number([]) // 0

{} - 1 // -1 {} 被当成了代码块

3、运算符

==

等于运算符我们也是经常使用的,我们应该更好的了解它的判别原理,防止较为隐藏的bug出现。

(1) type(x) == type(y)
    如果类型相同,那么以下情况返回true
    undefined == undefined
    10 == 10
    +0 == -0
    Null == Null
    "aaa" == "aaa"
    true == true
    false == false
    唯独下面这个返回false
    NaN == NaN
(2) null == undefined
(3) "2" == 2
(4) 引用类型需转为基本类型在比较
  • 栗子:
[''] == false 

(1) 需要将布尔类型转为数字类型,而false转为数字的结果是0,所以表达式变为:[''] == 0 
[''] == 0

(2) 需要将对象类型转为基本类型:首先调用[].valueOf(),由于数组的valueOf()方法返回自身,所以结果不是原始类型,继续调用[].toString()。对于数组来说,toString()方法的算法,是将每个元素都转为字符串类型,然后用逗号,依次连接起来,所以最终结果是空字符串'',它是一个原始类型的值。

'' == 0

(3) 需要将字符串类型转为数字类型,前面说了空字符串变成数字是0

0 == 0  // true

===

不仅数值相同,数据类型还必须相同。

总结

undefined == null,结果是true。且它俩与所有其他值比较的结果都是false。
String == Boolean,需要两个操作数同时转为Number。
String/Boolean == Number,需要String/Boolean转为Number。
Object == Primitive,需要Object转为Primitive(具体通过valueOf()和toString()方法)。


 [] == ![]  // true
 // 0 == !true
 // 0 == 0

未完待续~~